v0.12.0.2
This commit is contained in:
@@ -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,152 @@ 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 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) { }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,30 +10,32 @@ using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow }
|
||||
|
||||
partial class EnemyAIController : AIController
|
||||
{
|
||||
public static bool DisableEnemyAI;
|
||||
|
||||
private AIState _state;
|
||||
public AIState State
|
||||
{
|
||||
get { return _state; }
|
||||
set
|
||||
{
|
||||
if (_state == value) { return; }
|
||||
PreviousState = _state;
|
||||
OnStateChanged(_state, value);
|
||||
_state = value;
|
||||
}
|
||||
}
|
||||
|
||||
public AIState PreviousState { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable the character to attack the outposts and the characters inside them. Disabled by default in normal levels, enabled in outpost levels.
|
||||
/// </summary>
|
||||
public bool TargetOutposts;
|
||||
|
||||
// TODO: use a struct?
|
||||
class WallTarget
|
||||
{
|
||||
public Vector2 Position;
|
||||
public Structure Structure;
|
||||
public int SectionIndex;
|
||||
|
||||
public WallTarget(Vector2 position, Structure structure = null, int sectionIndex = -1)
|
||||
{
|
||||
Position = position;
|
||||
Structure = structure;
|
||||
SectionIndex = sectionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly float updateTargetsInterval = 1;
|
||||
private readonly float updateMemoriesInverval = 1;
|
||||
private readonly float attackLimbResetInterval = 2;
|
||||
@@ -56,9 +58,6 @@ namespace Barotrauma
|
||||
|
||||
private FishAnimController FishAnimController => Character.AnimController as FishAnimController;
|
||||
|
||||
//a point in a wall which the Character is currently targeting
|
||||
private WallTarget wallTarget;
|
||||
|
||||
//the limb selected for the current attack
|
||||
private Limb _attackingLimb;
|
||||
public Limb AttackingLimb
|
||||
@@ -159,6 +158,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly float maxSteeringBuffer = 5000;
|
||||
private readonly float minSteeringBuffer = 500;
|
||||
private readonly float steeringBufferIncreaseSpeed = 100;
|
||||
private float steeringBuffer;
|
||||
|
||||
public EnemyAIController(Character c, string seed) : base(c)
|
||||
{
|
||||
if (c.IsHuman)
|
||||
@@ -168,10 +172,9 @@ namespace Barotrauma
|
||||
if (Character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Pet
|
||||
Character.TeamID = Character.TeamType.FriendlyNPC;
|
||||
Character.TeamID = CharacterTeamType.FriendlyNPC;
|
||||
}
|
||||
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(c.SpeciesName);
|
||||
var mainElement = prefab.XDocument.Root.IsOverride() ? prefab.XDocument.Root.FirstElement() : prefab.XDocument.Root;
|
||||
var mainElement = c.Params.OriginalElement.IsOverride() ? c.Params.OriginalElement.FirstElement() : c.Params.OriginalElement;
|
||||
targetMemories = new Dictionary<AITarget, AITargetMemory>();
|
||||
steeringManager = outsideSteering;
|
||||
//allow targeting outposts and outpost NPCs in outpost levels
|
||||
@@ -188,7 +191,7 @@ namespace Barotrauma
|
||||
|
||||
if (aiElements.Count == 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in file \"" + prefab.FilePath + "\" - no AI element found.");
|
||||
DebugConsole.ThrowError("Error in file \"" + c.Params.File + "\" - no AI element found.");
|
||||
outsideSteering = new SteeringManager(this);
|
||||
insideSteering = new IndoorsSteeringManager(this, false, false);
|
||||
return;
|
||||
@@ -229,7 +232,7 @@ namespace Barotrauma
|
||||
|
||||
ReevaluateAttacks();
|
||||
outsideSteering = new SteeringManager(this);
|
||||
insideSteering = new IndoorsSteeringManager(this, false, canAttackDoors);
|
||||
insideSteering = new IndoorsSteeringManager(this, Character.IsHumanoid, canAttackDoors);
|
||||
steeringManager = outsideSteering;
|
||||
State = AIState.Idle;
|
||||
|
||||
@@ -242,7 +245,24 @@ namespace Barotrauma
|
||||
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody);
|
||||
}
|
||||
|
||||
public CharacterParams.AIParams AIParams => Character.Params.AI;
|
||||
private CharacterParams.AIParams _aiParams;
|
||||
public CharacterParams.AIParams AIParams
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_aiParams == null)
|
||||
{
|
||||
_aiParams = Character.Params.AI;
|
||||
if (_aiParams == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled.");
|
||||
Enabled = false;
|
||||
_aiParams = new CharacterParams.AIParams(null, Character.Params);
|
||||
}
|
||||
}
|
||||
return _aiParams;
|
||||
}
|
||||
}
|
||||
private CharacterParams.TargetParams GetTargetParams(string targetTag) => AIParams.GetTarget(targetTag, false);
|
||||
private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget));
|
||||
private string GetTargetingTag(AITarget aiTarget)
|
||||
@@ -321,7 +341,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private float movementMargin;
|
||||
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (DisableEnemyAI) { return; }
|
||||
@@ -341,14 +361,23 @@ namespace Barotrauma
|
||||
ignorePlatforms = height < allowedJumpHeight;
|
||||
}
|
||||
}
|
||||
if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent)
|
||||
{
|
||||
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
|
||||
}
|
||||
}
|
||||
Character.AnimController.IgnorePlatforms = ignorePlatforms;
|
||||
|
||||
//clients get the facing direction from the server
|
||||
if (Character.AnimController is HumanoidAnimController &&
|
||||
if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater &&
|
||||
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character))
|
||||
{
|
||||
if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater)
|
||||
if (SelectedAiTarget?.Entity != null || escapeTarget != null)
|
||||
{
|
||||
Entity t = SelectedAiTarget?.Entity ?? escapeTarget;
|
||||
float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
|
||||
Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left;
|
||||
}
|
||||
else
|
||||
{
|
||||
Character.AnimController.TargetDir = Character.AnimController.movement.X > 0.0f ? Direction.Right : Direction.Left;
|
||||
}
|
||||
@@ -389,7 +418,7 @@ namespace Barotrauma
|
||||
FadeMemories(updateMemoriesInverval);
|
||||
updateMemoriesTimer = updateMemoriesInverval;
|
||||
}
|
||||
if (Character.HealthPercentage <= FleeHealthThreshold && SelectedAiTarget != null &&
|
||||
if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null &&
|
||||
SelectedAiTarget.Entity is Character target && (target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target)))
|
||||
{
|
||||
// Keep fleeing if being chased
|
||||
@@ -408,7 +437,7 @@ namespace Barotrauma
|
||||
UpdateTargets(Character, out targetingParams);
|
||||
if (!IsLatchedOnSub)
|
||||
{
|
||||
UpdateWallTarget();
|
||||
UpdateWallTarget(requiredHoleCount);
|
||||
}
|
||||
updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
|
||||
if (SelectedAiTarget == null)
|
||||
@@ -423,21 +452,48 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (Character.CurrentHull == null)
|
||||
if (AIParams.Infiltrate)
|
||||
{
|
||||
if (steeringManager != outsideSteering)
|
||||
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
|
||||
|
||||
if (Character.Submarine != null || HasValidPath() && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer))
|
||||
{
|
||||
outsideSteering.Reset();
|
||||
if (steeringManager != insideSteering)
|
||||
{
|
||||
insideSteering.Reset();
|
||||
}
|
||||
steeringManager = insideSteering;
|
||||
steeringBuffer += steeringBufferIncreaseSpeed * deltaTime;
|
||||
}
|
||||
steeringManager = outsideSteering;
|
||||
else
|
||||
{
|
||||
if (steeringManager != outsideSteering)
|
||||
{
|
||||
outsideSteering.Reset();
|
||||
}
|
||||
steeringManager = outsideSteering;
|
||||
steeringBuffer = minSteeringBuffer;
|
||||
}
|
||||
steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (steeringManager != insideSteering)
|
||||
if (Character.Submarine != null)
|
||||
{
|
||||
insideSteering.Reset();
|
||||
if (steeringManager != insideSteering)
|
||||
{
|
||||
insideSteering.Reset();
|
||||
}
|
||||
steeringManager = insideSteering;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (steeringManager != outsideSteering)
|
||||
{
|
||||
outsideSteering.Reset();
|
||||
}
|
||||
steeringManager = outsideSteering;
|
||||
}
|
||||
steeringManager = insideSteering;
|
||||
}
|
||||
|
||||
bool useSteeringLengthAsMovementSpeed = State == AIState.Idle && Character.AnimController.InWater;
|
||||
@@ -519,12 +575,24 @@ namespace Barotrauma
|
||||
}
|
||||
if (State == AIState.Protect)
|
||||
{
|
||||
if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker && !attacker.Removed && !attacker.IsDead)
|
||||
if (SelectedAiTarget.Entity is Character targetCharacter)
|
||||
{
|
||||
// Attack the character that attacked the target we are protecting
|
||||
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
|
||||
SelectTarget(attacker.AiTarget);
|
||||
return;
|
||||
bool IsValid(Character.Attacker a)
|
||||
{
|
||||
Character c = a.Character;
|
||||
if (c.IsDead || c.Removed) { return false; }
|
||||
if (!IsFriendly(Character, c)) { return true; }
|
||||
// Only apply the threshold to friendly characters
|
||||
return a.Damage >= selectedTargetingParams.Threshold;
|
||||
}
|
||||
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character;
|
||||
if (attacker != null)
|
||||
{
|
||||
// Attack the character that attacked the target we are protecting
|
||||
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
|
||||
SelectTarget(attacker.AiTarget);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
|
||||
@@ -622,8 +690,8 @@ namespace Barotrauma
|
||||
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, targetMovement);
|
||||
if (Character.CurrentHull != null && Character.AnimController.InWater)
|
||||
{
|
||||
// Halve the swimming speed inside the sub
|
||||
Character.AnimController.TargetMovement *= 0.5f;
|
||||
// Limit the swimming speed inside the sub.
|
||||
Character.AnimController.TargetMovement = Character.AnimController.TargetMovement.ClampLength(5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,6 +759,8 @@ namespace Barotrauma
|
||||
#endregion
|
||||
|
||||
#region Escape
|
||||
private readonly float escapeTargetSeekInterval = 2;
|
||||
private float escapeTimer;
|
||||
private Gap escapeTarget;
|
||||
private bool allGapsSearched;
|
||||
private readonly HashSet<Gap> unreachableGaps = new HashSet<Gap>();
|
||||
@@ -707,35 +777,62 @@ namespace Barotrauma
|
||||
}
|
||||
IndoorsSteeringManager pathSteering = SteeringManager as IndoorsSteeringManager;
|
||||
bool hasValidPath = pathSteering?.CurrentPath != null && !pathSteering.IsPathDirty && !pathSteering.CurrentPath.Unreachable;
|
||||
if (allGapsSearched)
|
||||
{
|
||||
escapeTimer -= deltaTime;
|
||||
if (escapeTimer <= 0)
|
||||
{
|
||||
allGapsSearched = false;
|
||||
}
|
||||
}
|
||||
if (Character.CurrentHull != null && pathSteering != null)
|
||||
{
|
||||
// Seek exit if inside
|
||||
if (!allGapsSearched)
|
||||
{
|
||||
float closestDistance = 0;
|
||||
foreach (Gap gap in Gap.GapList)
|
||||
{
|
||||
if (gap == null || gap.Removed) { continue; }
|
||||
if (escapeTarget == gap) { continue; }
|
||||
if (unreachableGaps.Contains(gap)) { continue; }
|
||||
if (gap.Submarine != Character.Submarine) { continue; }
|
||||
if (gap.Open < 1 || gap.IsRoomToRoom) { continue; }
|
||||
bool canGetThrough = ConvertUnits.ToDisplayUnits(colliderWidth) < gap.Size;
|
||||
if (!canGetThrough) { continue; }
|
||||
if (escapeTarget == null)
|
||||
if (gap.IsRoomToRoom) { continue; }
|
||||
float multiplier = 1;
|
||||
var door = gap.ConnectedDoor;
|
||||
if (door != null)
|
||||
{
|
||||
escapeTarget = gap;
|
||||
if (!door.CanBeTraversed)
|
||||
{
|
||||
if (!door.HasAccess(Character))
|
||||
{
|
||||
if (!canAttackDoors) { continue; }
|
||||
// Treat doors that don't have access to like they were farther, because it will take time to break them.
|
||||
multiplier = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (gap.FlowTargetHull == Character.CurrentHull)
|
||||
else
|
||||
{
|
||||
if (gap.Open < 1) { continue; }
|
||||
bool canGetThrough = ConvertUnits.ToDisplayUnits(colliderWidth) < gap.Size;
|
||||
if (!canGetThrough) { continue; }
|
||||
}
|
||||
if (gap.FlowTargetHull == Character.CurrentHull)
|
||||
{
|
||||
// If the gap is in the same room, it's close enough.
|
||||
escapeTarget = gap;
|
||||
break;
|
||||
}
|
||||
else if (Vector2.DistanceSquared(Character.SimPosition, gap.SimPosition) < Vector2.DistanceSquared(Character.SimPosition, escapeTarget.SimPosition))
|
||||
float distance = Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) * multiplier;
|
||||
if (escapeTarget == null || distance < closestDistance)
|
||||
{
|
||||
escapeTarget = gap;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
allGapsSearched = true;
|
||||
escapeTimer = escapeTargetSeekInterval;
|
||||
}
|
||||
else if (escapeTarget != null && escapeTarget.FlowTargetHull != Character.CurrentHull)
|
||||
{
|
||||
@@ -760,36 +857,25 @@ namespace Barotrauma
|
||||
Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement);
|
||||
if (!MathUtils.IsValid(escapeDir)) { escapeDir = Vector2.UnitY; }
|
||||
SteeringManager.SteeringManual(deltaTime, escapeDir);
|
||||
return;
|
||||
}
|
||||
else if (pathSteering != null)
|
||||
{
|
||||
if (canAttackDoors && hasValidPath)
|
||||
if (hasValidPath && canAttackDoors)
|
||||
{
|
||||
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
|
||||
if (door != null && !door.IsOpen && !door.IsBroken)
|
||||
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))
|
||||
{
|
||||
if (SelectedAiTarget != door.Item.AiTarget)
|
||||
if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack)
|
||||
{
|
||||
SelectTarget(door.Item.AiTarget);
|
||||
SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority);
|
||||
State = AIState.Attack;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(escapeTarget.SimPosition, 5);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(escapeTarget.SimPosition, 5);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(escapeTarget.SimPosition, 10);
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
|
||||
}
|
||||
SteeringManager.SteeringSeek(escapeTarget.SimPosition, 10);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -860,49 +946,9 @@ namespace Barotrauma
|
||||
|
||||
if (Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
//targeting a wall section that can be passed through -> steer manually through the hole
|
||||
if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex))
|
||||
if (TrySteerThroughGaps(deltaTime))
|
||||
{
|
||||
WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex);
|
||||
Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, true);
|
||||
if (section?.gap != null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (SelectedAiTarget.Entity is Structure wall)
|
||||
{
|
||||
for (int i = 0; i < wall.Sections.Length; i++)
|
||||
{
|
||||
WallSection section = wall.Sections[i];
|
||||
if (CanPassThroughHole(wall, i) && section?.gap != null)
|
||||
{
|
||||
if (SteerThroughGap(wall, section, wall.SectionPosition(i, true), deltaTime))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (SelectedAiTarget.Entity is Item i)
|
||||
{
|
||||
var door = i.GetComponent<Door>();
|
||||
// Steer through the door manually if it's open or broken
|
||||
// Don't try to enter dry hulls if cannot walk or if the gap is too narrow
|
||||
if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.IsBroken))
|
||||
{
|
||||
if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25)
|
||||
{
|
||||
if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth))
|
||||
{
|
||||
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
|
||||
Character.AnimController.ReleaseStuckLimbs();
|
||||
var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition);
|
||||
steeringManager.SteeringManual(deltaTime, velocity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (SelectedAiTarget.Entity is Structure w && wallTarget == null)
|
||||
@@ -932,12 +978,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater &&
|
||||
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character))
|
||||
{
|
||||
Character.AnimController.TargetDir = Character.WorldPosition.X < attackWorldPos.X ? Direction.Right : Direction.Left;
|
||||
}
|
||||
|
||||
bool canAttack = true;
|
||||
bool pursue = false;
|
||||
if (IsCoolDownRunning)
|
||||
@@ -1119,30 +1159,34 @@ namespace Barotrauma
|
||||
}
|
||||
canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0;
|
||||
}
|
||||
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
|
||||
if (!AIParams.Infiltrate)
|
||||
{
|
||||
if (wallTarget == null && Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000)
|
||||
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
|
||||
{
|
||||
// Check that we are not bumping into a door or a wall
|
||||
Vector2 rayStart = SimPosition;
|
||||
if (Character.Submarine == null)
|
||||
if (wallTarget == null && Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000)
|
||||
{
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
Vector2 dir = SelectedAiTarget.WorldPosition - WorldPosition;
|
||||
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2);
|
||||
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
|
||||
if (Submarine.LastPickedFraction != 1.0f && closestBody != null &&
|
||||
(!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent<Door>() != null))
|
||||
{
|
||||
// Target is unreachable, there's a door or wall ahead
|
||||
State = AIState.Idle;
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
return;
|
||||
// Check that we are not bumping into a door or a wall
|
||||
Vector2 rayStart = SimPosition;
|
||||
if (Character.Submarine == null)
|
||||
{
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
Vector2 dir = SelectedAiTarget.WorldPosition - WorldPosition;
|
||||
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2);
|
||||
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
|
||||
if (Submarine.LastPickedFraction != 1.0f && closestBody != null &&
|
||||
(!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent<Door>() != null))
|
||||
{
|
||||
// Target is unreachable, there's a door or wall ahead
|
||||
State = AIState.Idle;
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float distance = 0;
|
||||
Limb attackTargetLimb = null;
|
||||
Character targetCharacter = SelectedAiTarget.Entity as Character;
|
||||
@@ -1202,6 +1246,16 @@ namespace Barotrauma
|
||||
// Check that we can reach the target
|
||||
distance = toTarget.Length();
|
||||
canAttack = distance < AttackingLimb.attack.Range;
|
||||
|
||||
// Crouch if the target is down (only humanoids), so that we can reach it.
|
||||
if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackingLimb.attack.Range * 2)
|
||||
{
|
||||
if (Math.Abs(toTarget.Y) > AttackingLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackingLimb.attack.Range)
|
||||
{
|
||||
humanoidAnimController.Crouching = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (canAttack)
|
||||
{
|
||||
if (AttackingLimb.attack.Ranged)
|
||||
@@ -1303,26 +1357,27 @@ namespace Barotrauma
|
||||
if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull)
|
||||
{
|
||||
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
|
||||
if (door != null && !door.IsOpen && !door.IsBroken)
|
||||
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))
|
||||
{
|
||||
if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget)
|
||||
{
|
||||
SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority);
|
||||
State = AIState.Attack;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Steer towards the target if in the same room and swimming
|
||||
if ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
|
||||
(targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity)))
|
||||
if (Character.CurrentHull != null && ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
|
||||
(targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))))
|
||||
{
|
||||
Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition;
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos));
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(steerPos, 2);
|
||||
pathSteering.SteeringSeek(steerPos, 2, startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), checkVisiblity: true);
|
||||
// Switch to Idle when cannot reach the target and if cannot damage the walls
|
||||
if ((!canAttackWalls || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
@@ -1363,6 +1418,16 @@ namespace Barotrauma
|
||||
}
|
||||
if (canAttack)
|
||||
{
|
||||
if (SelectedAiTarget.Entity is Item targetItem)
|
||||
{
|
||||
var door = targetItem.GetComponent<Door>();
|
||||
if (door != null && door.CanBeTraversed)
|
||||
{
|
||||
ResetAITarget();
|
||||
State = PreviousState;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb))
|
||||
{
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
@@ -1370,38 +1435,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSteeringThroughGap { get; private set; }
|
||||
private bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
|
||||
{
|
||||
IsSteeringThroughGap = true;
|
||||
wallTarget = null;
|
||||
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
|
||||
Character.AnimController.ReleaseStuckLimbs();
|
||||
Hull targetHull = section.gap?.FlowTargetHull;
|
||||
float maxDistance = Math.Min(wall.Rect.Width, wall.Rect.Height);
|
||||
if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (targetHull != null)
|
||||
{
|
||||
// If already inside, target the hull, else target the wall.
|
||||
SelectedAiTarget = Character.CurrentHull != null ? targetHull.AiTarget : wall.AiTarget;
|
||||
if (wall.IsHorizontal)
|
||||
{
|
||||
targetWorldPos.Y = targetHull.WorldRect.Y - targetHull.Rect.Height / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetWorldPos.X = targetHull.WorldRect.Center.X;
|
||||
}
|
||||
steeringManager.SteeringManual(deltaTime, Vector2.Normalize(targetWorldPos - Character.WorldPosition));
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private readonly List<Limb> attackLimbs = new List<Limb>();
|
||||
private readonly List<float> weights = new List<float>();
|
||||
private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null)
|
||||
@@ -1471,106 +1504,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateWallTarget()
|
||||
{
|
||||
wallTarget = null;
|
||||
if (SelectedAiTarget == null) { return; }
|
||||
if (SelectedAiTarget.Entity == null) { return; }
|
||||
//check if there's a wall between the target and the Character
|
||||
Vector2 rayStart = SimPosition;
|
||||
Vector2 rayEnd = SelectedAiTarget.SimPosition;
|
||||
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
|
||||
{
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
|
||||
{
|
||||
rayEnd -= Character.Submarine.SimPosition;
|
||||
}
|
||||
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine);
|
||||
if (Submarine.LastPickedFraction != 1.0f && closestBody != null)
|
||||
{
|
||||
if (closestBody.UserData is Structure wall && wall.Submarine != null && (wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts))
|
||||
{
|
||||
int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
|
||||
float sectionDamage = wall.SectionDamage(sectionIndex);
|
||||
for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++)
|
||||
{
|
||||
if (wall.SectionBodyDisabled(i))
|
||||
{
|
||||
if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i))
|
||||
{
|
||||
sectionIndex = i;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
//otherwise ignore and keep breaking other sections
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (wall.SectionDamage(i) > sectionDamage)
|
||||
{
|
||||
sectionIndex = i;
|
||||
}
|
||||
}
|
||||
Vector2 sectionPos = wall.SectionPosition(sectionIndex);
|
||||
Vector2 attachTargetNormal;
|
||||
if (wall.IsHorizontal)
|
||||
{
|
||||
attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y));
|
||||
sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y;
|
||||
}
|
||||
else
|
||||
{
|
||||
attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f);
|
||||
sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
|
||||
}
|
||||
LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
|
||||
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
|
||||
{
|
||||
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine)
|
||||
{
|
||||
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
// Blocked by a wall that shouldn't be targeted. The main intention here is to prevents monsters from entering the the tail and the nose pieces.
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
}
|
||||
else
|
||||
{
|
||||
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null)
|
||||
{
|
||||
if (closestBody.UserData is Structure w && w.Submarine != null || closestBody.UserData is Item i && i.Submarine != null)
|
||||
{
|
||||
// Cannot reach the target, because it's blocked by a disabled wall or a door
|
||||
State = AIState.Idle;
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
public override void OnAttacked(Character attacker, AttackResult attackResult)
|
||||
{
|
||||
float reactionTime = Rand.Range(0.1f, 0.3f);
|
||||
@@ -1603,7 +1536,7 @@ namespace Barotrauma
|
||||
if (!isFriendly && attackResult.Damage > 0.0f)
|
||||
{
|
||||
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls;
|
||||
if (Character.Params.AI.AttackWhenProvoked && canAttack)
|
||||
if (AIParams.AttackWhenProvoked && canAttack)
|
||||
{
|
||||
if (attacker.IsHusk)
|
||||
{
|
||||
@@ -1661,7 +1594,7 @@ namespace Barotrauma
|
||||
|
||||
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
|
||||
bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine;
|
||||
bool avoidGunFire = Character.Params.AI.AvoidGunfire && attacker.Submarine != Character.Submarine;
|
||||
bool avoidGunFire = AIParams.AvoidGunfire && attacker.Submarine != Character.Submarine;
|
||||
|
||||
if (State == AIState.Attack && !IsCoolDownRunning)
|
||||
{
|
||||
@@ -1686,7 +1619,7 @@ namespace Barotrauma
|
||||
avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f);
|
||||
SelectTarget(attacker.AiTarget);
|
||||
}
|
||||
if (Character.HealthPercentage <= FleeHealthThreshold)
|
||||
if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold)
|
||||
{
|
||||
State = AIState.Flee;
|
||||
avoidTimer = AIParams.MinFleeTime * Rand.Range(0.75f, 1.25f);
|
||||
@@ -1707,6 +1640,7 @@ namespace Barotrauma
|
||||
if (aiTarget != null && SelectedAiTarget != aiTarget)
|
||||
{
|
||||
SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, true).Priority);
|
||||
State = AIState.Attack;
|
||||
}
|
||||
}
|
||||
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
|
||||
@@ -1974,7 +1908,7 @@ namespace Barotrauma
|
||||
bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null;
|
||||
if (targetingFromOutsideToInside)
|
||||
{
|
||||
if (door != null && !canAttackDoors || !canAttackWalls)
|
||||
if (door != null && (!canAttackDoors && !AIParams.Infiltrate) || !canAttackWalls)
|
||||
{
|
||||
// Can't reach
|
||||
continue;
|
||||
@@ -2122,16 +2056,14 @@ namespace Barotrauma
|
||||
bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom;
|
||||
// Ignore inner doors when outside
|
||||
if (character.CurrentHull == null && !isOutdoor) { continue; }
|
||||
bool isOpen = door.IsOpen || door.IsBroken;
|
||||
if (!isOpen && !canAttackDoors || (isOutdoor && !AIParams.TargetOuterWalls))
|
||||
bool isOpen = door.CanBeTraversed;
|
||||
if (!isOpen)
|
||||
{
|
||||
// Ignore doors that are not open if cannot attack doors or shouldn't target outer doors.
|
||||
continue;
|
||||
if (!canAttackDoors || isOutdoor && !AIParams.TargetOuterWalls) { continue; }
|
||||
}
|
||||
if (isOpen && (!Character.AnimController.CanEnterSubmarine || !AggressiveBoarding))
|
||||
else if (!Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
// Ignore broken and open doors
|
||||
// Aggressive boarders don't ignore open doors, because they use them for getting in.
|
||||
// Ignore broken and open doors, if cannot enter submarine
|
||||
continue;
|
||||
}
|
||||
if (AggressiveBoarding)
|
||||
@@ -2157,8 +2089,9 @@ namespace Barotrauma
|
||||
if (targetingTag == null) { continue; }
|
||||
var targetParams = GetTargetParams(targetingTag);
|
||||
if (targetParams == null) { continue; }
|
||||
if (targetParams.IgnoreWhileInside && character.CurrentHull != null) { continue; }
|
||||
if (targetParams.IgnoreWhileOutside && character.CurrentHull == null) { continue; }
|
||||
if (targetParams.IgnoreInside && character.CurrentHull != null) { continue; }
|
||||
if (targetParams.IgnoreOutside && character.CurrentHull == null) { continue; }
|
||||
if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; }
|
||||
if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat)
|
||||
{
|
||||
if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine)
|
||||
@@ -2270,7 +2203,7 @@ namespace Barotrauma
|
||||
foreach (var gap in Character.CurrentHull.ConnectedGaps)
|
||||
{
|
||||
var door = gap.ConnectedDoor;
|
||||
if (door == null || !door.IsOpen && !door.IsBroken)
|
||||
if (door == null)
|
||||
{
|
||||
var wall = gap.ConnectedWall;
|
||||
if (wall != null)
|
||||
@@ -2293,7 +2226,7 @@ namespace Barotrauma
|
||||
newTarget = aiTarget;
|
||||
selectedTargetMemory = targetMemory;
|
||||
targetValue = valueModifier;
|
||||
targetingParams = GetTargetParams(targetingTag);
|
||||
targetingParams = targetParams;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2332,6 +2265,149 @@ namespace Barotrauma
|
||||
return SelectedAiTarget;
|
||||
}
|
||||
|
||||
class WallTarget
|
||||
{
|
||||
public Vector2 Position;
|
||||
public Structure Structure;
|
||||
public int SectionIndex;
|
||||
|
||||
public WallTarget(Vector2 position, Structure structure = null, int sectionIndex = -1)
|
||||
{
|
||||
Position = position;
|
||||
Structure = structure;
|
||||
SectionIndex = sectionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private WallTarget wallTarget;
|
||||
|
||||
private void UpdateWallTarget(int requiredHoleCount)
|
||||
{
|
||||
wallTarget = null;
|
||||
if (State == AIState.Flee || State == AIState.Escape) { return; }
|
||||
if (AIParams.Infiltrate && HasValidPath(requireNonDirty: true)) { return; }
|
||||
if (SelectedAiTarget == null) { return; }
|
||||
if (SelectedAiTarget.Entity == null) { return; }
|
||||
Vector2 rayStart = SimPosition;
|
||||
Vector2 rayEnd = SelectedAiTarget.SimPosition;
|
||||
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
|
||||
{
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
|
||||
{
|
||||
rayEnd -= Character.Submarine.SimPosition;
|
||||
}
|
||||
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine);
|
||||
if (Submarine.LastPickedFraction != 1.0f && closestBody != null)
|
||||
{
|
||||
if (closestBody.UserData is Structure wall && wall.Submarine != null && (Character.IsBot || wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts))
|
||||
{
|
||||
int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
|
||||
float sectionDamage = wall.SectionDamage(sectionIndex);
|
||||
for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++)
|
||||
{
|
||||
if (wall.SectionBodyDisabled(i))
|
||||
{
|
||||
if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount))
|
||||
{
|
||||
sectionIndex = i;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ignore and keep breaking other sections
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (wall.SectionDamage(i) > sectionDamage)
|
||||
{
|
||||
sectionIndex = i;
|
||||
}
|
||||
}
|
||||
Vector2 sectionPos = wall.SectionPosition(sectionIndex);
|
||||
Vector2 attachTargetNormal;
|
||||
if (wall.IsHorizontal)
|
||||
{
|
||||
attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y));
|
||||
sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y;
|
||||
}
|
||||
else
|
||||
{
|
||||
attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f);
|
||||
sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
|
||||
}
|
||||
LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
|
||||
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
|
||||
{
|
||||
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine)
|
||||
{
|
||||
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
// Blocked by a wall that shouldn't be targeted. The main intention here is to prevents monsters from entering the the tail and the nose pieces.
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
}
|
||||
else
|
||||
{
|
||||
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null)
|
||||
{
|
||||
if (closestBody.UserData is Structure w && w.Submarine != null || closestBody.UserData is Item i && i.Submarine != null)
|
||||
{
|
||||
// Cannot reach the target, because it's blocked by a disabled wall or a door
|
||||
State = AIState.Idle;
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TrySteerThroughGaps(float deltaTime)
|
||||
{
|
||||
if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex, requiredHoleCount))
|
||||
{
|
||||
WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex);
|
||||
Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, true);
|
||||
return section?.gap != null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime);
|
||||
}
|
||||
else if (SelectedAiTarget != null)
|
||||
{
|
||||
if (SelectedAiTarget.Entity is Structure wall)
|
||||
{
|
||||
for (int i = 0; i < wall.Sections.Length; i++)
|
||||
{
|
||||
WallSection section = wall.Sections[i];
|
||||
if (CanPassThroughHole(wall, i, requiredHoleCount) && section?.gap != null)
|
||||
{
|
||||
return SteerThroughGap(wall, section, wall.SectionPosition(i, true), deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (SelectedAiTarget.Entity is Item i)
|
||||
{
|
||||
var door = i.GetComponent<Door>();
|
||||
// Don't try to enter dry hulls if cannot walk or if the gap is too narrow
|
||||
if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && door.CanBeTraversed)
|
||||
{
|
||||
if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25)
|
||||
{
|
||||
if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth))
|
||||
{
|
||||
return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound)
|
||||
{
|
||||
if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
|
||||
@@ -2631,28 +2707,38 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanPassThroughHole(Structure wall, int sectionIndex)
|
||||
public override bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
|
||||
{
|
||||
if (!wall.SectionBodyDisabled(sectionIndex)) return false;
|
||||
int holeCount = 1;
|
||||
for (int j = sectionIndex - 1; j > sectionIndex - requiredHoleCount; j--)
|
||||
wallTarget = null;
|
||||
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
|
||||
Character.AnimController.ReleaseStuckLimbs();
|
||||
bool success = base.SteerThroughGap(wall, section, targetWorldPos, deltaTime);
|
||||
if (success)
|
||||
{
|
||||
if (wall.SectionBodyDisabled(j))
|
||||
holeCount++;
|
||||
else
|
||||
break;
|
||||
// If already inside, target the hull, else target the wall.
|
||||
SelectedAiTarget = Character.CurrentHull != null ? section.gap.AiTarget : wall.AiTarget;
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
|
||||
}
|
||||
for (int j = sectionIndex + 1; j < sectionIndex + requiredHoleCount; j++)
|
||||
{
|
||||
if (wall.SectionBodyDisabled(j))
|
||||
holeCount++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
return holeCount >= requiredHoleCount;
|
||||
IsSteeringThroughGap = success;
|
||||
return success;
|
||||
}
|
||||
|
||||
public override bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance = -1)
|
||||
{
|
||||
wallTarget = null;
|
||||
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
|
||||
Character.AnimController.ReleaseStuckLimbs();
|
||||
bool success = base.SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance);
|
||||
if (success)
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
|
||||
}
|
||||
IsSteeringThroughGap = success;
|
||||
return success;
|
||||
}
|
||||
|
||||
public bool CanPassThroughHole(Structure wall, int sectionIndex) => CanPassThroughHole(wall, sectionIndex, requiredHoleCount);
|
||||
|
||||
private readonly List<Limb> targetLimbs = new List<Limb>();
|
||||
public Limb GetTargetLimb(Limb attackLimb, Character target, LimbType targetLimbType = LimbType.None)
|
||||
{
|
||||
|
||||
@@ -29,7 +29,9 @@ namespace Barotrauma
|
||||
private float flipTimer;
|
||||
private const float FlipInterval = 0.5f;
|
||||
|
||||
public static float HULL_SAFETY_THRESHOLD = 50;
|
||||
public const float HULL_SAFETY_THRESHOLD = 40;
|
||||
public const float HULL_LOW_OXYGEN_PERCENTAGE = 30;
|
||||
|
||||
private static readonly float characterWaitOnSwitch = 5;
|
||||
|
||||
public readonly HashSet<Hull> UnreachableHulls = new HashSet<Hull>();
|
||||
@@ -60,10 +62,7 @@ namespace Barotrauma
|
||||
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
|
||||
public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
|
||||
|
||||
public override AIObjectiveManager ObjectiveManager
|
||||
{
|
||||
get { return objectiveManager; }
|
||||
}
|
||||
public AIObjectiveManager ObjectiveManager => objectiveManager;
|
||||
|
||||
public Order CurrentOrder
|
||||
{
|
||||
@@ -79,9 +78,7 @@ namespace Barotrauma
|
||||
|
||||
public float CurrentHullSafety { get; private set; } = 100;
|
||||
|
||||
private readonly Dictionary<Character, float> damageDoneByAttacker = new Dictionary<Character, float>();
|
||||
private readonly HashSet<Character> attackers = new HashSet<Character>();
|
||||
|
||||
private readonly Dictionary<Character, float> structureDamageAccumulator = new Dictionary<Character, float>();
|
||||
private readonly Dictionary<Hull, HullSafety> knownHulls = new Dictionary<Hull, HullSafety>();
|
||||
private class HullSafety
|
||||
{
|
||||
@@ -132,22 +129,6 @@ namespace Barotrauma
|
||||
{
|
||||
if (DisableCrewAI || Character.Removed) { return; }
|
||||
|
||||
//slowly forget about damage done by attackers
|
||||
foreach (Character enemy in attackers)
|
||||
{
|
||||
float cumulativeDamage = damageDoneByAttacker[enemy];
|
||||
if (cumulativeDamage > 0)
|
||||
{
|
||||
float reduction = deltaTime;
|
||||
if (cumulativeDamage < 2)
|
||||
{
|
||||
// If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage)
|
||||
reduction *= 0.5f;
|
||||
}
|
||||
damageDoneByAttacker[enemy] -= reduction;
|
||||
}
|
||||
}
|
||||
|
||||
bool isIncapacitated = Character.IsIncapacitated;
|
||||
if (freezeAI && !isIncapacitated)
|
||||
{
|
||||
@@ -188,7 +169,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
|
||||
bool hasValidPath = steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable;
|
||||
bool hasValidPath = HasValidPath();
|
||||
|
||||
if (Character.Submarine == null && hasValidPath)
|
||||
{
|
||||
@@ -259,7 +240,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (Character.CurrentHull != null)
|
||||
{
|
||||
if (Character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
// Outpost npcs don't inform each other about threads, like crew members do.
|
||||
VisibleHulls.ForEach(h => RefreshHullSafety(h));
|
||||
@@ -386,10 +367,11 @@ namespace Barotrauma
|
||||
|
||||
if (isCarrying)
|
||||
{
|
||||
if (findItemState != FindItemState.OtherItem)
|
||||
if (findItemState == FindItemState.DivingSuit && ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
|
||||
{
|
||||
if (ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective))
|
||||
{
|
||||
// Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker.
|
||||
gotoObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
@@ -414,7 +396,7 @@ namespace Barotrauma
|
||||
{
|
||||
shouldKeepTheGearOn = false;
|
||||
}
|
||||
else if (Character.CurrentHull.Oxygen < CharacterHealth.LowOxygenThreshold)
|
||||
else if (Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10)
|
||||
{
|
||||
shouldKeepTheGearOn = true;
|
||||
}
|
||||
@@ -558,39 +540,37 @@ namespace Barotrauma
|
||||
// Other items
|
||||
if (isCarrying) { return; }
|
||||
if (!ObjectiveManager.CurrentObjective.AllowAutomaticItemUnequipping || !ObjectiveManager.GetActiveObjective().AllowAutomaticItemUnequipping) { return; }
|
||||
foreach (var item in Character.Inventory.Items)
|
||||
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
|
||||
{
|
||||
if (item == null) { continue; }
|
||||
if (Character.HasEquippedItem(item) &&
|
||||
(Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) ||
|
||||
Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) ||
|
||||
Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand)))
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var hand = i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand;
|
||||
Item item = Character.Inventory.GetItemInLimbSlot(hand);
|
||||
if (item == null) { continue; }
|
||||
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
|
||||
findItemState = FindItemState.OtherItem;
|
||||
if (FindSuitableContainer(item, out Item targetContainer))
|
||||
{
|
||||
findItemState = FindItemState.OtherItem;
|
||||
if (FindSuitableContainer(item, out Item targetContainer))
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () =>
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () =>
|
||||
{
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Drop(Character);
|
||||
HandleRelocation(item);
|
||||
}
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Drop(Character);
|
||||
HandleRelocation(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,7 +582,7 @@ namespace Barotrauma
|
||||
|
||||
private void HandleRelocation(Item item)
|
||||
{
|
||||
if (item.Submarine?.TeamID == Character.TeamType.FriendlyNPC)
|
||||
if (item.Submarine?.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (itemsToRelocate.Contains(item)) { return; }
|
||||
itemsToRelocate.Add(item);
|
||||
@@ -627,7 +607,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (item.ParentInventory.Owner is Character c)
|
||||
{
|
||||
if (c.TeamID == Character.TeamType.Team1 || c.TeamID == Character.TeamType.Team2)
|
||||
if (c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2)
|
||||
{
|
||||
// Taken by a player/bot (if npc or monster would take the item, we'd probably still want it to spawn back to the main sub.
|
||||
return;
|
||||
@@ -654,18 +634,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private enum FindItemState
|
||||
{
|
||||
None,
|
||||
@@ -681,23 +649,23 @@ namespace Barotrauma
|
||||
public static bool FindSuitableContainer(Character character, Item containableItem, List<Item> ignoredItems, ref int itemIndex, out Item suitableContainer)
|
||||
{
|
||||
suitableContainer = null;
|
||||
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, customPriorityFunction: i =>
|
||||
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i =>
|
||||
{
|
||||
if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; }
|
||||
var container = i.GetComponent<ItemContainer>();
|
||||
if (container == null) { return 0; }
|
||||
if (container.Inventory.IsFull()) { return 0; }
|
||||
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
|
||||
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
|
||||
{
|
||||
if (isRestrictionsDefined)
|
||||
{
|
||||
return 4;
|
||||
return 10;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (containableItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
|
||||
if (containableItem.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
|
||||
{
|
||||
return isPreferencesDefined ? isSecondary ? 2 : 3 : 1;
|
||||
return isPreferencesDefined ? isSecondary ? 2 : 5 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -749,6 +717,18 @@ namespace Barotrauma
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList)
|
||||
{
|
||||
if (ballastFlora.Parent?.Submarine != Character.Submarine) { continue; }
|
||||
if (!ballastFlora.HasBrokenThrough) { continue; }
|
||||
// Don't react to the first two branches, because they are usually in the very edges of the room.
|
||||
if (ballastFlora.Branches.Count(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull) > 2)
|
||||
{
|
||||
var orderPrefab = Order.GetPrefab("reportballastflora");
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
if (!isFighting)
|
||||
{
|
||||
foreach (var gap in hull.ConnectedGaps)
|
||||
@@ -798,7 +778,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (newOrder != null)
|
||||
{
|
||||
if (Character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default,
|
||||
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
|
||||
@@ -837,8 +817,8 @@ namespace Barotrauma
|
||||
Character.Speak(TextManager.Get("DialogBleeding"), null, Rand.Range(0.5f, 5.0f), "bleeding", 30.0f);
|
||||
}
|
||||
|
||||
if (Character.PressureTimer > 50.0f && Character.CurrentHull != null)
|
||||
{
|
||||
if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null)
|
||||
{
|
||||
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, true), null, Rand.Range(0.5f, 5.0f), "pressure", 30.0f);
|
||||
}
|
||||
}
|
||||
@@ -895,15 +875,6 @@ namespace Barotrauma
|
||||
if (totalDamage <= 0.01f) { return; }
|
||||
if (Character.IsBot)
|
||||
{
|
||||
if (attacker != null)
|
||||
{
|
||||
if (!damageDoneByAttacker.ContainsKey(attacker))
|
||||
{
|
||||
damageDoneByAttacker[attacker] = 0.0f;
|
||||
}
|
||||
damageDoneByAttacker[attacker] += totalDamage;
|
||||
attackers.Add(attacker);
|
||||
}
|
||||
if (!freezeAI && !Character.IsDead && Character.IsIncapacitated)
|
||||
{
|
||||
// Removes the combat objective and resets all objectives.
|
||||
@@ -953,7 +924,7 @@ namespace Barotrauma
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed ||
|
||||
otherCharacter.Info?.Job == null || otherCharacter.TeamID != Character.TeamType.FriendlyNPC ||
|
||||
otherCharacter.Info?.Job == null || otherCharacter.TeamID != CharacterTeamType.FriendlyNPC ||
|
||||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
|
||||
otherCharacter.IsInstigator)
|
||||
{
|
||||
@@ -1045,7 +1016,7 @@ namespace Barotrauma
|
||||
// The guards don't react when the player attacks instigators.
|
||||
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
|
||||
}
|
||||
else if (attacker.TeamID == Character.TeamType.FriendlyNPC)
|
||||
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (c.IsSecurity)
|
||||
{
|
||||
@@ -1078,7 +1049,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, bool allowHoldFire = false)
|
||||
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
|
||||
{
|
||||
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
|
||||
if (Character.IsDead || Character.IsIncapacitated) { return; }
|
||||
@@ -1117,49 +1088,19 @@ namespace Barotrauma
|
||||
{
|
||||
objective.Abandoned += onAbort;
|
||||
}
|
||||
if (onCompleted != null)
|
||||
{
|
||||
objective.Completed += onCompleted;
|
||||
}
|
||||
return objective;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOrder(Order order, string option, Character orderGiver, bool speak = true)
|
||||
{
|
||||
CurrentOrderOption = option;
|
||||
CurrentOrder = order;
|
||||
objectiveManager.SetOrder(order, option, orderGiver);
|
||||
if (ObjectiveManager.CurrentOrder != null && speak && Character.SpeechImpediment < 100.0f)
|
||||
{
|
||||
if (ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
|
||||
}
|
||||
else if (ObjectiveManager.CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
|
||||
}
|
||||
else
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
|
||||
}
|
||||
}
|
||||
objectiveManager.SetOrder(order, option, orderGiver, speak);
|
||||
}
|
||||
|
||||
public override void SelectTarget(AITarget target)
|
||||
@@ -1213,56 +1154,6 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
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 (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.Items.Length; 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.Items[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 static bool NeedsDivingGear(Hull hull, out bool needsSuit)
|
||||
{
|
||||
needsSuit = false;
|
||||
@@ -1274,7 +1165,7 @@ namespace Barotrauma
|
||||
needsSuit = true;
|
||||
return true;
|
||||
}
|
||||
if (hull.WaterPercentage > 60 || hull.Oxygen < CharacterHealth.LowOxygenThreshold)
|
||||
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -1294,20 +1185,118 @@ namespace Barotrauma
|
||||
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
|
||||
|
||||
private static List<Item> matchingItems = new List<Item>();
|
||||
public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable<Item> items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false)
|
||||
|
||||
/// <summary>
|
||||
/// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second.
|
||||
/// To solve this, create a copy of the collection or change the code so that you first handle the first items and only after that query for the next items.
|
||||
/// </summary>
|
||||
public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable<Item> items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func<Item, bool> predicate = null)
|
||||
{
|
||||
matchingItems.Clear();
|
||||
items = matchingItems;
|
||||
if (character == null) { return false; }
|
||||
if (character.Inventory == null) { return false; }
|
||||
matchingItems = character.Inventory.FindAllItems(i => i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier), recursive: true, matchingItems);
|
||||
items = matchingItems;
|
||||
return matchingItems.Any(i => i != null &&
|
||||
matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
|
||||
i.ConditionPercentage >= conditionPercentage &&
|
||||
(!requireEquipped || character.HasEquippedItem(i)) &&
|
||||
(containedTag == null ||
|
||||
(i.OwnInventory?.Items != null &&
|
||||
i.OwnInventory.Items.Any(it => it != null && it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))));
|
||||
(predicate == null || predicate(i)), recursive, matchingItems);
|
||||
items = matchingItems;
|
||||
return matchingItems.Any(i => i != null && (containedTag == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)));
|
||||
}
|
||||
|
||||
public static void StructureDamaged(Structure structure, float damageAmount, Character character)
|
||||
{
|
||||
const float MaxDamagePerSecond = 5.0f;
|
||||
const float MaxDamagePerFrame = MaxDamagePerSecond * (float)Timing.Step;
|
||||
|
||||
const float WarningThreshold = 5.0f;
|
||||
const float ArrestThreshold = 20.0f;
|
||||
const float KillThreshold = 50.0f;
|
||||
|
||||
if (character == null || damageAmount <= 0.0f) { return; }
|
||||
if (structure?.Submarine == null || !structure.Submarine.Info.IsOutpost || character.TeamID == structure.Submarine.TeamID) { return; }
|
||||
//structure not indestructible = something that's "meant" to be destroyed, like an ice wall in mines
|
||||
if (!structure.Prefab.IndestructibleInOutposts) { return; }
|
||||
|
||||
bool someoneSpoke = false;
|
||||
float maxAccumulatedDamage = 0.0f;
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead ||
|
||||
otherCharacter.Info?.Job == null ||
|
||||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
|
||||
!otherHumanAI.VisibleHulls.Contains(character.CurrentHull))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!otherCharacter.CanSeeCharacter(character)) { continue; }
|
||||
|
||||
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
|
||||
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
|
||||
otherHumanAI.structureDamageAccumulator[character] += MathHelper.Clamp(damageAmount, -MaxDamagePerFrame, MaxDamagePerFrame);
|
||||
float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
|
||||
maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage);
|
||||
|
||||
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
|
||||
{
|
||||
var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage;
|
||||
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.Value -= reputationLoss;
|
||||
}
|
||||
|
||||
if (accumulatedDamage <= WarningThreshold) { return; }
|
||||
|
||||
if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
|
||||
!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
|
||||
{
|
||||
//if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene
|
||||
if (accumulatedDamage < ArrestThreshold)
|
||||
{
|
||||
if (otherHumanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
|
||||
{
|
||||
(otherHumanAI.ObjectiveManager.CurrentObjective as AIObjectiveIdle)?.FaceTargetAndWait(character, 5.0f);
|
||||
}
|
||||
}
|
||||
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning"), null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls", 10.0f);
|
||||
someoneSpoke = true;
|
||||
}
|
||||
// React if we are security
|
||||
if ((accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
|
||||
(accumulatedDamage > KillThreshold && prevAccumulatedDamage <= KillThreshold))
|
||||
{
|
||||
var combatMode = accumulatedDamage > KillThreshold ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
|
||||
if (!TriggerSecurity(otherHumanAI, combatMode))
|
||||
{
|
||||
// Else call the others
|
||||
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
|
||||
{
|
||||
if (!TriggerSecurity(security.AIController as HumanAIController, combatMode))
|
||||
{
|
||||
// Only alert one guard at a time
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TriggerSecurity(HumanAIController humanAI, AIObjectiveCombat.CombatMode combatMode)
|
||||
{
|
||||
if (humanAI == null) { return false; }
|
||||
if (!humanAI.Character.IsSecurity) { return false; }
|
||||
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
|
||||
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(), allowHoldFire: true, onCompleted: () =>
|
||||
{
|
||||
//if the target is arrested successfully, reset the damage accumulator
|
||||
foreach (Character anyCharacter in Character.CharacterList)
|
||||
{
|
||||
if (anyCharacter.AIController is HumanAIController anyAI)
|
||||
{
|
||||
anyAI.structureDamageAccumulator?.Remove(character);
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ItemTaken(Item item, Character character)
|
||||
@@ -1316,7 +1305,7 @@ namespace Barotrauma
|
||||
Character thief = character;
|
||||
bool someoneSpoke = false;
|
||||
|
||||
if (item.SpawnedInOutpost && thief.TeamID != Character.TeamType.FriendlyNPC && !item.HasTag("handlocker"))
|
||||
if (item.SpawnedInOutpost && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
|
||||
{
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
@@ -1495,11 +1484,13 @@ namespace Barotrauma
|
||||
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
|
||||
}
|
||||
|
||||
public float GetDamageDoneByAttacker(Character attacker)
|
||||
public float GetDamageDoneByAttacker(Character otherCharacter)
|
||||
{
|
||||
if (!damageDoneByAttacker.TryGetValue(attacker, out float dmg))
|
||||
float dmg = 0;
|
||||
Character.Attacker attacker = Character.LastAttackers.LastOrDefault(a => a.Character == otherCharacter);
|
||||
if (attacker != null)
|
||||
{
|
||||
dmg = 0;
|
||||
dmg = attacker.Damage;
|
||||
}
|
||||
return dmg;
|
||||
}
|
||||
@@ -1550,9 +1541,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (hull == null) { return 0; }
|
||||
if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; }
|
||||
// TODO: take the visiblehulls into account?
|
||||
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp(0.25f, 1, hull.OxygenPercentage / 100);
|
||||
float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, 0.25f, hull.WaterPercentage / 100);
|
||||
// Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
|
||||
// With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
|
||||
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
|
||||
float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, hull.WaterPercentage / 100);
|
||||
if (!character.NeedsAir)
|
||||
{
|
||||
oxygenFactor = 1;
|
||||
@@ -1637,7 +1629,7 @@ namespace Barotrauma
|
||||
if (!teamGood) { return false; }
|
||||
bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
|
||||
if (!speciesGood) { return false; }
|
||||
if (me.TeamID == Character.TeamType.FriendlyNPC && other.TeamID == Character.TeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
var reputation = campaign.Map?.CurrentLocation?.Reputation;
|
||||
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
|
||||
@@ -1651,7 +1643,7 @@ namespace Barotrauma
|
||||
private static bool IsOnFriendlyTeam(GameMode mode, Character me, Character other)
|
||||
{
|
||||
// Only enemies are in the Team "None"
|
||||
bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.None;
|
||||
bool friendlyTeam = me.TeamID != CharacterTeamType.None && other.TeamID != CharacterTeamType.None;
|
||||
// When playing a combat mission, we need to be on the same team to be friendlies
|
||||
if (friendlyTeam && mode is MissionMode mm && mm.Mission is CombatMission)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ namespace Barotrauma
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -295,7 +295,7 @@ namespace Barotrauma
|
||||
// Only humanoids can climb ladders
|
||||
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
|
||||
Ladder currentLadder = currentPath.CurrentNode.Ladders;
|
||||
if (currentLadder != null && currentLadder.Item.NonInteractable)
|
||||
if (currentLadder != null && !currentLadder.Item.IsInteractable(character))
|
||||
{
|
||||
currentLadder = null;
|
||||
}
|
||||
@@ -303,7 +303,7 @@ namespace Barotrauma
|
||||
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))
|
||||
{
|
||||
@@ -361,7 +361,7 @@ namespace Barotrauma
|
||||
nextLadder.Item.TryInteract(character, false, true);
|
||||
}
|
||||
}
|
||||
if (nextLadder != null || isAboveFloor)
|
||||
if (isAboveFloor || nextLadderSameAsCurrent)
|
||||
{
|
||||
currentPath.SkipToNextNode();
|
||||
}
|
||||
@@ -387,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;
|
||||
@@ -421,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();
|
||||
}
|
||||
@@ -438,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,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)
|
||||
@@ -645,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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -220,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);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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; }
|
||||
|
||||
+5
@@ -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;
|
||||
|
||||
+30
-16
@@ -12,15 +12,24 @@ 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.CurrentOrder == this ? AIObjectiveManager.OrderPriority : AIObjectiveManager.RunPriority - 1) : 0;
|
||||
|
||||
protected override bool Filter(Item target)
|
||||
{
|
||||
@@ -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 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; }
|
||||
@@ -96,18 +112,16 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var slotType in inv.SlotTypes)
|
||||
{
|
||||
if (allowedSlot.HasFlag(slotType))
|
||||
if (!allowedSlot.HasFlag(slotType)) { continue; }
|
||||
for (int i = 0; i < inv.Capacity; i++)
|
||||
{
|
||||
for (int i = 0; i < inv.Capacity; i++)
|
||||
canEquip = true;
|
||||
if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null)
|
||||
{
|
||||
canEquip = true;
|
||||
if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.Items[i] != null)
|
||||
{
|
||||
canEquip = false;
|
||||
break;
|
||||
}
|
||||
canEquip = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-33
@@ -140,7 +140,7 @@ namespace Barotrauma
|
||||
|
||||
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))
|
||||
{
|
||||
@@ -238,7 +238,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 +262,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 +306,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 +371,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 +566,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 +599,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,7 +618,7 @@ 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);
|
||||
@@ -651,7 +652,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);
|
||||
}
|
||||
}
|
||||
@@ -724,7 +725,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 +770,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 +815,50 @@ 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 (WeaponComponent == null) { return false; }
|
||||
if (Weapon.OwnInventory == null) { return true; }
|
||||
// Eject empty ammo
|
||||
if (Weapon.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0)
|
||||
foreach (Item containedItem in Weapon.OwnInventory.AllItemsMod)
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
if (containedItem.Condition <= 0)
|
||||
{
|
||||
if (character.Submarine == null)
|
||||
{
|
||||
// If we are outside of main sub, try to put the ammo in the inventory instead dropping it in the sea.
|
||||
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
+6
-7
@@ -67,14 +67,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++;
|
||||
}
|
||||
@@ -83,7 +83,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)
|
||||
{
|
||||
@@ -102,11 +102,10 @@ namespace Barotrauma
|
||||
}
|
||||
if (character.CanInteractWith(container.Item, checkLinked: false))
|
||||
{
|
||||
if (RemoveEmpty)
|
||||
if (RemoveEmpty && container.Inventory.AllItems.Any(it => it.Condition <= 0.0f))
|
||||
{
|
||||
foreach (var emptyItem in container.Inventory.Items)
|
||||
foreach (var emptyItem in container.Inventory.AllItemsMod)
|
||||
{
|
||||
if (emptyItem == null) { continue; }
|
||||
if (emptyItem.Condition <= 0)
|
||||
{
|
||||
emptyItem.Drop(character);
|
||||
|
||||
+7
-2
@@ -58,12 +58,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 +82,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (targetContainer.Inventory.Items.Contains(itemToDecontain))
|
||||
else if (targetContainer.Inventory.Contains(itemToDecontain))
|
||||
{
|
||||
IsCompleted = true;
|
||||
return;
|
||||
|
||||
+5
-4
@@ -111,9 +111,10 @@ namespace Barotrauma
|
||||
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;
|
||||
bool canSee = HumanAIController.VisibleHulls.Contains(fs.Hull) || character.CanSeeTarget(fs);
|
||||
bool move = !inRange || !canSee;
|
||||
if ((inRange && canSee) || useExtinquisherTimer > 0)
|
||||
// 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)
|
||||
@@ -148,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
|
||||
|
||||
-1
@@ -38,7 +38,6 @@ 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; }
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+20
-12
@@ -1,5 +1,6 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -56,7 +57,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");
|
||||
@@ -70,8 +71,11 @@ namespace Barotrauma
|
||||
// Seek oxygen that has min 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 (!HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 10))
|
||||
{
|
||||
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,
|
||||
@@ -83,7 +87,7 @@ namespace Barotrauma
|
||||
// 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
|
||||
@@ -100,18 +104,22 @@ 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)
|
||||
containedItems = target.OwnInventory?.AllItems;
|
||||
if (containedItems == null) { return false; }
|
||||
foreach (Item containedItem in target.OwnInventory.AllItemsMod)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach (Item containedItem in containedItems)
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
{
|
||||
if (actor.Submarine == null)
|
||||
{
|
||||
// If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea.
|
||||
if (actor.Inventory.TryPutItem(containedItem, actor, CharacterInventory.anySlot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
containedItem.Drop(actor);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -168,7 +168,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 +359,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;
|
||||
}
|
||||
|
||||
+11
-9
@@ -64,7 +64,7 @@ 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>())
|
||||
@@ -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,17 +87,20 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
// Drop empty tanks
|
||||
foreach (Item containedItem in containedItems)
|
||||
if (weldingTool.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
foreach (Item containedItem in weldingTool.OwnInventory.AllItemsMod)
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (containedItems.None(i => i != null && i.HasTag("weldingfuel") && i.Condition > 0.0f))
|
||||
|
||||
if (weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
|
||||
{
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref refuelObjective));
|
||||
return;
|
||||
|
||||
+8
-4
@@ -177,7 +177,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (moveToTarget is Item parentItem)
|
||||
{
|
||||
canInteract = character.CanInteractWith(parentItem, out _, checkLinked: false);
|
||||
canInteract = character.CanInteractWith(parentItem, checkLinked: false);
|
||||
}
|
||||
if (canInteract)
|
||||
{
|
||||
@@ -256,7 +256,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 +276,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 +312,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 +351,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; }
|
||||
|
||||
+154
-47
@@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -34,6 +35,9 @@ namespace Barotrauma
|
||||
public float extraDistanceOutsideSub;
|
||||
private float _closeEnough = 50;
|
||||
private readonly float minDistance = 50;
|
||||
private readonly float seekGapsInterval = 1;
|
||||
private float seekGapsTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Display units
|
||||
/// </summary>
|
||||
@@ -116,6 +120,8 @@ namespace Barotrauma
|
||||
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)
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
@@ -286,52 +287,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 +430,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 +441,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 +461,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 +522,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
|
||||
@@ -507,6 +611,7 @@ namespace Barotrauma
|
||||
{
|
||||
PathSteering.ResetPath();
|
||||
}
|
||||
SpeakCannotReach();
|
||||
base.OnAbandon();
|
||||
}
|
||||
|
||||
@@ -530,6 +635,8 @@ namespace Barotrauma
|
||||
{
|
||||
base.Reset();
|
||||
findDivingGear = null;
|
||||
seekGapsTimer = 0;
|
||||
TargetGap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-4
@@ -21,7 +21,7 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
behavior = value;
|
||||
if (behavior == BehaviorType.StayInHull && character.TeamID != Character.TeamType.FriendlyNPC)
|
||||
if (behavior == BehaviorType.StayInHull && character.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
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);
|
||||
behavior = BehaviorType.Passive;
|
||||
@@ -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 =>
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
+71
-9
@@ -127,10 +127,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)
|
||||
{
|
||||
@@ -273,7 +277,8 @@ namespace Barotrauma
|
||||
CurrentOrder = objective;
|
||||
}
|
||||
|
||||
public void SetOrder(Order order, string option, Character orderGiver)
|
||||
private CoroutineHandle speakRoutine;
|
||||
public void SetOrder(Order order, string option, Character orderGiver, bool speak)
|
||||
{
|
||||
if (character.IsDead)
|
||||
{
|
||||
@@ -294,6 +299,49 @@ namespace Barotrauma
|
||||
{
|
||||
// This should be redundant, because all the objectives are reset when they are selected as active.
|
||||
CurrentOrder.Reset();
|
||||
if (speak)
|
||||
{
|
||||
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 (CurrentOrder != null && character.SpeechImpediment < 100.0f)
|
||||
{
|
||||
if (CurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
|
||||
}
|
||||
else if (CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
|
||||
}
|
||||
}
|
||||
}, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,8 +368,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 +392,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 +417,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)
|
||||
{
|
||||
@@ -403,11 +450,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)
|
||||
{
|
||||
|
||||
+10
-23
@@ -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))
|
||||
{
|
||||
@@ -101,7 +101,8 @@ 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;
|
||||
}
|
||||
@@ -137,7 +138,7 @@ namespace Barotrauma
|
||||
throw new Exception("target null");
|
||||
#endif
|
||||
}
|
||||
else if (target.Item.NonInteractable)
|
||||
else if (!target.Item.IsInteractable(character))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
@@ -157,21 +158,6 @@ namespace Barotrauma
|
||||
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 +201,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 +227,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))
|
||||
|
||||
+1
-1
@@ -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; }
|
||||
|
||||
+26
-11
@@ -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?
|
||||
@@ -107,8 +111,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 +119,39 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
// Drop empty tanks
|
||||
foreach (Item containedItem in containedItems)
|
||||
// Eject empty tanks
|
||||
if (repairTool.Item.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
foreach (Item containedItem in repairTool.Item.OwnInventory.AllItemsMod)
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
{
|
||||
if (character.Submarine == null)
|
||||
{
|
||||
// If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea.
|
||||
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -229,7 +244,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))
|
||||
{
|
||||
|
||||
+1
-1
@@ -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; }
|
||||
|
||||
+16
-5
@@ -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.
|
||||
@@ -331,9 +331,13 @@ 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;
|
||||
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,6 +431,13 @@ namespace Barotrauma
|
||||
replaceOxygenObjective = null;
|
||||
safeHull = null;
|
||||
ignoreOxygen = false;
|
||||
character.SelectedCharacter = null;
|
||||
}
|
||||
|
||||
public override void OnDeselected()
|
||||
{
|
||||
character.SelectedCharacter = null;
|
||||
base.OnDeselected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,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 +101,6 @@ namespace Barotrauma
|
||||
public bool TargetAllCharacters { get; }
|
||||
public bool IsReport => TargetAllCharacters && !MustSetTarget;
|
||||
|
||||
|
||||
public readonly float FadeOutTime;
|
||||
|
||||
public Entity TargetEntity;
|
||||
@@ -119,9 +122,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 +162,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 +247,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 +276,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 +325,7 @@ namespace Barotrauma
|
||||
IsPrefab = true;
|
||||
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
|
||||
IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis";
|
||||
DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -324,23 +335,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 +365,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;
|
||||
}
|
||||
}
|
||||
@@ -433,7 +445,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 +469,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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -20,8 +20,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, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
|
||||
: base(prefab, speciesName, position, seed, characterInfo, id: Entity.NullEntityID, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll)
|
||||
{
|
||||
InitProjSpecific();
|
||||
}
|
||||
|
||||
+19
-4
@@ -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)
|
||||
|
||||
+19
-21
@@ -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();
|
||||
}
|
||||
@@ -1459,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
|
||||
@@ -1467,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1771,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];
|
||||
@@ -1786,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;
|
||||
@@ -1805,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];
|
||||
|
||||
@@ -1853,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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2032,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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,11 +121,26 @@ 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; set; }
|
||||
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 LevelWallDamage { get; set; }
|
||||
@@ -280,7 +295,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)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics.Joints;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using Barotrauma.IO;
|
||||
@@ -16,6 +15,14 @@ using System.Text;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public enum CharacterTeamType
|
||||
{
|
||||
None = 0,
|
||||
Team1 = 1,
|
||||
Team2 = 2,
|
||||
FriendlyNPC = 3
|
||||
}
|
||||
|
||||
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerSerializable
|
||||
{
|
||||
public static List<Character> CharacterList = new List<Character>();
|
||||
@@ -101,18 +108,9 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
protected Key[] keys;
|
||||
private readonly Item[] selectedItems;
|
||||
|
||||
public enum TeamType
|
||||
{
|
||||
None,
|
||||
Team1,
|
||||
Team2,
|
||||
FriendlyNPC
|
||||
}
|
||||
|
||||
private TeamType teamID;
|
||||
public TeamType TeamID
|
||||
private CharacterTeamType teamID;
|
||||
public CharacterTeamType TeamID
|
||||
{
|
||||
get { return teamID; }
|
||||
set
|
||||
@@ -122,6 +120,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOnPlayerTeam => TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2;
|
||||
|
||||
public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator;
|
||||
public CombatAction CombatAction;
|
||||
|
||||
@@ -135,7 +135,25 @@ namespace Barotrauma
|
||||
public readonly string Seed;
|
||||
protected Item focusedItem;
|
||||
private Character selectedCharacter, selectedBy;
|
||||
public Character LastAttacker;
|
||||
|
||||
private const int maxLastAttackerCount = 4;
|
||||
|
||||
public class Attacker
|
||||
{
|
||||
public Character Character;
|
||||
public float Damage;
|
||||
}
|
||||
|
||||
private readonly List<Attacker> lastAttackers = new List<Attacker>();
|
||||
public IEnumerable<Attacker> LastAttackers
|
||||
{
|
||||
get { return lastAttackers; }
|
||||
}
|
||||
public Character LastAttacker
|
||||
{
|
||||
get { return lastAttackers.Count > 0 ? lastAttackers[lastAttackers.Count - 1].Character : null; }
|
||||
}
|
||||
|
||||
public Entity LastDamageSource;
|
||||
|
||||
public float InvisibleTimer;
|
||||
@@ -260,6 +278,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public string VariantOf { get; private set; }
|
||||
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
@@ -429,6 +449,20 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Items the character has in their hand slots. Doesn't return nulls and only returns items held in both hands once.
|
||||
/// </summary>
|
||||
public IEnumerable<Item> HeldItems
|
||||
{
|
||||
get
|
||||
{
|
||||
var item1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand);
|
||||
var item2 = Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand);
|
||||
if (item1 != null) { yield return item1; }
|
||||
if (item2 != null && item2 != item1) { yield return item2; }
|
||||
}
|
||||
}
|
||||
|
||||
private float lowPassMultiplier;
|
||||
public float LowPassMultiplier
|
||||
{
|
||||
@@ -445,7 +479,7 @@ namespace Barotrauma
|
||||
}
|
||||
set
|
||||
{
|
||||
obstructVisionAmount = 1.0f;
|
||||
obstructVisionAmount = value ? 1.0f : 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,6 +532,8 @@ namespace Barotrauma
|
||||
get { return oxygenAvailable; }
|
||||
set { oxygenAvailable = MathHelper.Clamp(value, 0.0f, 100.0f); }
|
||||
}
|
||||
|
||||
public bool UseHullOxygen { get; set; } = true;
|
||||
|
||||
public float Stun
|
||||
{
|
||||
@@ -575,9 +611,12 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
public Item[] SelectedItems
|
||||
/// <summary>
|
||||
/// Current speed of the character's collider. Can be used by status effects to check if the character is moving.
|
||||
/// </summary>
|
||||
public float CurrentSpeed
|
||||
{
|
||||
get { return selectedItems; }
|
||||
get { return AnimController?.Collider?.LinearVelocity.Length() ?? 0.0f; }
|
||||
}
|
||||
|
||||
private Item _selectedConstruction;
|
||||
@@ -620,7 +659,23 @@ namespace Barotrauma
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
public bool IsDead { get; private set; }
|
||||
private bool isDead;
|
||||
public bool IsDead
|
||||
{
|
||||
get { return isDead; }
|
||||
set
|
||||
{
|
||||
if (isDead == value) { return; }
|
||||
if (value)
|
||||
{
|
||||
Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null);
|
||||
}
|
||||
else
|
||||
{
|
||||
Revive();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsObserving => AIController is EnemyAIController enemyAI && enemyAI.Enabled && enemyAI.State == AIState.Observe;
|
||||
|
||||
@@ -666,7 +721,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return (IsDead || Stun > 0.0f || LockHands || IsIncapacitated);
|
||||
return IsDead || Stun > 0.0f || LockHands || IsIncapacitated;
|
||||
}
|
||||
}
|
||||
set { canInventoryBeAccessed = value; }
|
||||
@@ -680,6 +735,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool InWater => AnimController?.InWater ?? false;
|
||||
|
||||
public bool GodMode = false;
|
||||
|
||||
public CampaignMode.InteractionType CampaignInteractionType;
|
||||
@@ -770,7 +827,8 @@ namespace Barotrauma
|
||||
speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant();
|
||||
}
|
||||
|
||||
if (CharacterPrefab.FindBySpeciesName(speciesName) == null)
|
||||
var prefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (prefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to create character \"{speciesName}\". Matching prefab not found.\n" + Environment.StackTrace);
|
||||
return null;
|
||||
@@ -779,21 +837,21 @@ namespace Barotrauma
|
||||
Character newCharacter = null;
|
||||
if (!speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var aiCharacter = new AICharacter(speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll);
|
||||
var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll);
|
||||
var ai = new EnemyAIController(aiCharacter, seed);
|
||||
aiCharacter.SetAI(ai);
|
||||
newCharacter = aiCharacter;
|
||||
}
|
||||
else if (hasAi)
|
||||
{
|
||||
var aiCharacter = new AICharacter(speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll);
|
||||
var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll);
|
||||
var ai = new HumanAIController(aiCharacter);
|
||||
aiCharacter.SetAI(ai);
|
||||
newCharacter = aiCharacter;
|
||||
}
|
||||
else
|
||||
{
|
||||
newCharacter = new Character(speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isRemotePlayer, ragdollParams: ragdoll);
|
||||
newCharacter = new Character(prefab, speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isRemotePlayer, ragdollParams: ragdoll);
|
||||
}
|
||||
|
||||
float healthRegen = newCharacter.Params.Health.ConstantHealthRegeneration;
|
||||
@@ -833,16 +891,14 @@ namespace Barotrauma
|
||||
return newCharacter;
|
||||
}
|
||||
|
||||
protected Character(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null)
|
||||
protected Character(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null)
|
||||
: base(null, id)
|
||||
{
|
||||
prefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
|
||||
VariantOf = prefab.VariantOf;
|
||||
this.Seed = seed;
|
||||
this.prefab = prefab;
|
||||
MTRandom random = new MTRandom(ToolBox.StringToInt(seed));
|
||||
|
||||
selectedItems = new Item[2];
|
||||
|
||||
IsRemotePlayer = isRemotePlayer;
|
||||
|
||||
oxygenAvailable = 100.0f;
|
||||
@@ -851,11 +907,19 @@ namespace Barotrauma
|
||||
lowPassMultiplier = 1.0f;
|
||||
|
||||
Properties = SerializableProperty.GetProperties(this);
|
||||
|
||||
Params = new CharacterParams(prefab.FilePath);
|
||||
|
||||
Info = characterInfo;
|
||||
|
||||
speciesName = VariantOf ?? speciesName;
|
||||
|
||||
if (speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (VariantOf != null)
|
||||
{
|
||||
DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!");
|
||||
}
|
||||
if (characterInfo == null)
|
||||
{
|
||||
Info = new CharacterInfo(CharacterPrefab.HumanSpeciesName);
|
||||
@@ -873,6 +937,10 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var rootElement = prefab.XDocument.Root;
|
||||
if (VariantOf != null)
|
||||
{
|
||||
rootElement = CharacterPrefab.FindBySpeciesName(VariantOf)?.XDocument?.Root;
|
||||
}
|
||||
var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
|
||||
InitProjSpecific(mainElement);
|
||||
|
||||
@@ -897,6 +965,36 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Params.VariantFile != null)
|
||||
{
|
||||
XElement overrideElement = Params.VariantFile.Root;
|
||||
// Only override if the override file contains matching elements
|
||||
if (overrideElement.GetChildElement("inventory") != null)
|
||||
{
|
||||
inventoryElements.Clear();
|
||||
inventoryCommonness.Clear();
|
||||
foreach (XElement subElement in overrideElement.GetChildElements("inventory"))
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "inventory":
|
||||
inventoryElements.Add(subElement);
|
||||
inventoryCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (overrideElement.GetChildElement("health") != null)
|
||||
{
|
||||
healthElements.Clear();
|
||||
healthCommonness.Clear();
|
||||
foreach (XElement subElement in overrideElement.GetChildElements("health"))
|
||||
{
|
||||
healthElements.Add(subElement);
|
||||
healthCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inventoryElements.Count > 0)
|
||||
{
|
||||
@@ -910,9 +1008,14 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
CharacterHealth = new CharacterHealth(
|
||||
healthElements.Count == 1 ? healthElements[0] : ToolBox.SelectWeightedRandom(healthElements, healthCommonness, random),
|
||||
this);
|
||||
var selectedHealthElement = healthElements.Count == 1 ? healthElements[0] : ToolBox.SelectWeightedRandom(healthElements, healthCommonness, random);
|
||||
// If there's no limb elements defined in the override variant, let's use the limb health definitions of the original file.
|
||||
var limbHealthElement = selectedHealthElement;
|
||||
if (Params.VariantFile != null && limbHealthElement.GetChildElement("limb") == null)
|
||||
{
|
||||
limbHealthElement = Params.OriginalElement.GetChildElement("health");
|
||||
}
|
||||
CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement);
|
||||
}
|
||||
|
||||
if (Params.Husk)
|
||||
@@ -928,6 +1031,7 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError("Cannot find a husk infection that matches this species! Please add the speciesnames as 'targets' in the husk affliction prefab definition!");
|
||||
// Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg.
|
||||
nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler";
|
||||
speciesName = nonHuskedSpeciesName;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1161,7 +1265,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (info?.Job == null || spawnPoint == null) { return; }
|
||||
|
||||
foreach (Item item in Inventory.Items)
|
||||
foreach (Item item in Inventory.AllItems)
|
||||
{
|
||||
if (item?.Prefab.Identifier != "idcard") { continue; }
|
||||
foreach (string s in spawnPoint.IdCardTags)
|
||||
@@ -1558,12 +1662,8 @@ namespace Barotrauma
|
||||
|
||||
if (SelectedConstruction == null || !SelectedConstruction.Prefab.DisableItemUsageWhenSelected)
|
||||
{
|
||||
for (int i = 0; i < selectedItems.Length; i++)
|
||||
foreach (Item item in HeldItems)
|
||||
{
|
||||
if (selectedItems[i] == null) { continue; }
|
||||
if (i == 1 && selectedItems[0] == selectedItems[1]) { continue; }
|
||||
var item = selectedItems[i];
|
||||
if (item == null) { continue; }
|
||||
if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse)
|
||||
{
|
||||
item.SecondaryUse(deltaTime, this);
|
||||
@@ -1712,7 +1812,7 @@ namespace Barotrauma
|
||||
var door = item.GetComponent<Door>();
|
||||
if (door != null)
|
||||
{
|
||||
return !door.IsOpen && !door.IsBroken;
|
||||
return !door.CanBeTraversed;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -1739,7 +1839,7 @@ namespace Barotrauma
|
||||
Structure wall = closestBody.UserData as Structure;
|
||||
Item item = closestBody.UserData as Item;
|
||||
Door door = item?.GetComponent<Door>();
|
||||
return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen || door.IsBroken);
|
||||
return (wall == null || !wall.CastShadow) && (door == null || door.CanBeTraversed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1754,9 +1854,8 @@ namespace Barotrauma
|
||||
if (Inventory == null) { return false; }
|
||||
for (int i = 0; i < Inventory.Capacity; i++)
|
||||
{
|
||||
if (Inventory.Items[i] == item && Inventory.SlotTypes[i] != InvSlotType.Any) { return true; }
|
||||
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i) == item) { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1765,55 +1864,15 @@ namespace Barotrauma
|
||||
if (Inventory == null) { return false; }
|
||||
for (int i = 0; i < Inventory.Capacity; i++)
|
||||
{
|
||||
if (Inventory.SlotTypes[i] == InvSlotType.Any || Inventory.Items[i] == null) { continue; }
|
||||
if (!allowBroken && Inventory.Items[i].Condition <= 0.0f) { continue; }
|
||||
if (Inventory.Items[i].Prefab.Identifier == tagOrIdentifier || Inventory.Items[i].HasTag(tagOrIdentifier)) { return true; }
|
||||
if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; }
|
||||
var item = Inventory.GetItemAt(i);
|
||||
if (item == null) { continue; }
|
||||
if (!allowBroken && item.Condition <= 0.0f) { continue; }
|
||||
if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool HasSelectedItem(Item item)
|
||||
{
|
||||
return selectedItems.Contains(item);
|
||||
}
|
||||
|
||||
public bool TrySelectItem(Item item)
|
||||
{
|
||||
bool rightHand = Inventory.IsInLimbSlot(item, InvSlotType.RightHand);
|
||||
bool leftHand = Inventory.IsInLimbSlot(item, InvSlotType.LeftHand);
|
||||
|
||||
bool selected = false;
|
||||
if (rightHand && (selectedItems[0] == null || selectedItems[0] == item))
|
||||
{
|
||||
selectedItems[0] = item;
|
||||
selected = true;
|
||||
}
|
||||
if (leftHand && (selectedItems[1] == null || selectedItems[1] == item))
|
||||
{
|
||||
selectedItems[1] = item;
|
||||
selected = true;
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
public bool TrySelectItem(Item item, int index)
|
||||
{
|
||||
if (selectedItems[index] != null) { return false; }
|
||||
|
||||
selectedItems[index] = item;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void DeselectItem(Item item)
|
||||
{
|
||||
for (int i = 0; i < selectedItems.Length; i++)
|
||||
{
|
||||
if (selectedItems[i] == item) selectedItems[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanAccessInventory(Inventory inventory)
|
||||
{
|
||||
if (!CanInteract || inventory.Locked) { return false; }
|
||||
@@ -1848,7 +1907,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable<string> identifiers = null, bool ignoreBroken = true,
|
||||
IEnumerable<Item> ignoredItems = null, IEnumerable<string> ignoredContainerIdentifiers = null,
|
||||
Func<Item, bool> customPredicate = null, Func<Item, float> customPriorityFunction = null, float maxItemDistance = 10000)
|
||||
Func<Item, bool> customPredicate = null, Func<Item, float> customPriorityFunction = null, float maxItemDistance = 10000, ISpatialEntity positionalReference = null)
|
||||
{
|
||||
if (itemIndex == 0)
|
||||
{
|
||||
@@ -1859,7 +1918,7 @@ namespace Barotrauma
|
||||
{
|
||||
itemIndex++;
|
||||
var item = Item.ItemList[itemIndex];
|
||||
if (item.NonInteractable) { continue; }
|
||||
if (!item.IsInteractable(this)) { continue; }
|
||||
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
|
||||
if (item.Submarine == null) { continue; }
|
||||
if (item.Submarine.TeamID != TeamID) { continue; }
|
||||
@@ -1879,10 +1938,15 @@ namespace Barotrauma
|
||||
float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1;
|
||||
if (itemPriority <= 0) { continue; }
|
||||
Entity rootInventoryOwner = item.GetRootInventoryOwner();
|
||||
if (rootInventoryOwner is Item ownerItem)
|
||||
{
|
||||
if (!ownerItem.IsInteractable(this)) { continue; }
|
||||
}
|
||||
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
|
||||
float yDist = Math.Abs(WorldPosition.Y - itemPos.Y);
|
||||
Vector2 refPos = positionalReference != null ? positionalReference.WorldPosition : WorldPosition;
|
||||
float yDist = Math.Abs(refPos.Y - itemPos.Y);
|
||||
yDist = yDist > 100 ? yDist * 5 : 0;
|
||||
float dist = Math.Abs(WorldPosition.X - itemPos.X) + yDist;
|
||||
float dist = Math.Abs(refPos.X - itemPos.X) + yDist;
|
||||
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, maxItemDistance, dist));
|
||||
itemPriority *= distanceFactor;
|
||||
if (itemPriority > _selectedItemPriority)
|
||||
@@ -1924,7 +1988,7 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
|
||||
#endif
|
||||
if (!CanInteract || hidden || item.NonInteractable) { return false; }
|
||||
if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; }
|
||||
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
@@ -2115,7 +2179,7 @@ namespace Barotrauma
|
||||
FocusedCharacter = CanInteract ? FindCharacterAtPosition(mouseSimPos) : null;
|
||||
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
|
||||
float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
|
||||
if (SelectedItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
|
||||
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
|
||||
{
|
||||
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
|
||||
aimAssist = 0.0f;
|
||||
@@ -2350,10 +2414,9 @@ namespace Barotrauma
|
||||
|
||||
if (Inventory != null)
|
||||
{
|
||||
foreach (Item item in Inventory.Items)
|
||||
foreach (Item item in Inventory.AllItems)
|
||||
{
|
||||
if (item == null || item.body == null || item.body.Enabled) { continue; }
|
||||
|
||||
if (item.body == null || item.body.Enabled) { continue; }
|
||||
item.SetTransform(SimPosition, 0.0f);
|
||||
item.Submarine = Submarine;
|
||||
}
|
||||
@@ -2361,10 +2424,11 @@ namespace Barotrauma
|
||||
|
||||
HideFace = false;
|
||||
|
||||
|
||||
UpdateSightRange(deltaTime);
|
||||
UpdateSoundRange(deltaTime);
|
||||
|
||||
UpdateAttackers(deltaTime);
|
||||
|
||||
if (IsDead) { return; }
|
||||
|
||||
if (GameMain.NetworkMember != null)
|
||||
@@ -2523,6 +2587,56 @@ namespace Barotrauma
|
||||
|
||||
partial void SetOrderProjSpecific(Order order, string orderOption);
|
||||
|
||||
|
||||
public void AddAttacker(Character character, float damage)
|
||||
{
|
||||
Attacker attacker = lastAttackers.FirstOrDefault(a => a.Character == character);
|
||||
if (attacker != null)
|
||||
{
|
||||
lastAttackers.Remove(attacker);
|
||||
}
|
||||
else
|
||||
{
|
||||
attacker = new Attacker { Character = character };
|
||||
}
|
||||
|
||||
if (lastAttackers.Count > maxLastAttackerCount)
|
||||
{
|
||||
lastAttackers.RemoveRange(0, lastAttackers.Count - maxLastAttackerCount);
|
||||
}
|
||||
|
||||
attacker.Damage += damage;
|
||||
lastAttackers.Add(attacker);
|
||||
}
|
||||
|
||||
public void ForgiveAttacker(Character character)
|
||||
{
|
||||
int index;
|
||||
if ((index = lastAttackers.FindIndex(a => a.Character == character)) >= 0)
|
||||
{
|
||||
lastAttackers.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAttackers(float deltaTime)
|
||||
{
|
||||
//slowly forget about damage done by attackers
|
||||
foreach (Attacker enemy in LastAttackers)
|
||||
{
|
||||
float cumulativeDamage = enemy.Damage;
|
||||
if (cumulativeDamage > 0)
|
||||
{
|
||||
float reduction = deltaTime;
|
||||
if (cumulativeDamage < 2)
|
||||
{
|
||||
// If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage)
|
||||
reduction *= 0.5f;
|
||||
}
|
||||
enemy.Damage = Math.Max(0.0f, enemy.Damage-reduction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOxygen(float deltaTime)
|
||||
{
|
||||
if (NeedsAir)
|
||||
@@ -2545,7 +2659,7 @@ namespace Barotrauma
|
||||
{
|
||||
//don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull
|
||||
//(i.e. if the character has some external source of oxygen)
|
||||
if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage)
|
||||
if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage && UseHullOxygen)
|
||||
{
|
||||
AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime;
|
||||
}
|
||||
@@ -2554,6 +2668,7 @@ namespace Barotrauma
|
||||
}
|
||||
OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
|
||||
}
|
||||
UseHullOxygen = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2661,18 +2776,16 @@ namespace Barotrauma
|
||||
|
||||
void onItemContainerSpawned(Item item)
|
||||
{
|
||||
if (Inventory?.Items == null) { return; }
|
||||
if (Inventory == null) { return; }
|
||||
|
||||
item.UpdateTransform();
|
||||
|
||||
item.UpdateTransform();
|
||||
item.AddTag("name:" + Name);
|
||||
if (info?.Job != null) { item.AddTag("job:" + info.Job.Name); }
|
||||
|
||||
var itemContainer = item?.GetComponent<ItemContainer>();
|
||||
if (itemContainer == null) { return; }
|
||||
foreach (Item inventoryItem in Inventory.Items)
|
||||
foreach (Item inventoryItem in Inventory.AllItemsMod)
|
||||
{
|
||||
if (inventoryItem == null) { continue; }
|
||||
if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null))
|
||||
{
|
||||
//if the item couldn't be put inside the despawn container, just drop it
|
||||
@@ -2907,8 +3020,8 @@ namespace Barotrauma
|
||||
float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime;
|
||||
|
||||
var attackResult = targetLimb == null ?
|
||||
AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker) :
|
||||
DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker);
|
||||
AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier) :
|
||||
DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier);
|
||||
|
||||
if (limbHit == null) { return new AttackResult(); }
|
||||
Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld;
|
||||
@@ -3000,7 +3113,7 @@ namespace Barotrauma
|
||||
return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker);
|
||||
}
|
||||
|
||||
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null)
|
||||
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1)
|
||||
{
|
||||
hitLimb = null;
|
||||
|
||||
@@ -3022,10 +3135,26 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
return DamageLimb(worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker);
|
||||
return DamageLimb(worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker, damageMultiplier);
|
||||
}
|
||||
|
||||
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null)
|
||||
public void RecordKill(Character target)
|
||||
{
|
||||
if (!IsOnPlayerTeam) { return; }
|
||||
if (GameMain.Config.KilledCreatures.Any(name => name.Equals(target.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; }
|
||||
GameMain.Config.KilledCreatures.Add(target.SpeciesName);
|
||||
AddEncounter(target);
|
||||
}
|
||||
|
||||
public void AddEncounter(Character other)
|
||||
{
|
||||
if (!IsOnPlayerTeam) { return; }
|
||||
if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(other.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; }
|
||||
GameMain.Config.EncounteredCreatures.Add(other.SpeciesName);
|
||||
GameMain.Config.RecentlyEncounteredCreatures.Add(other.SpeciesName);
|
||||
}
|
||||
|
||||
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1)
|
||||
{
|
||||
if (Removed) { return new AttackResult(); }
|
||||
|
||||
@@ -3069,7 +3198,7 @@ namespace Barotrauma
|
||||
}
|
||||
bool wasDead = IsDead;
|
||||
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
|
||||
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound);
|
||||
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier);
|
||||
CharacterHealth.ApplyDamage(hitLimb, attackResult);
|
||||
if (attacker != this)
|
||||
{
|
||||
@@ -3078,6 +3207,10 @@ namespace Barotrauma
|
||||
if (!wasDead)
|
||||
{
|
||||
TryAdjustAttackerSkill(attacker, -attackResult.Damage);
|
||||
if (IsDead)
|
||||
{
|
||||
attacker?.RecordKill(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (attackResult.Damage > 0)
|
||||
@@ -3086,7 +3219,9 @@ namespace Barotrauma
|
||||
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
|
||||
if (attacker != null)
|
||||
{
|
||||
LastAttacker = attacker;
|
||||
AddAttacker(attacker, attackResult.Damage);
|
||||
AddEncounter(attacker);
|
||||
attacker.AddEncounter(this);
|
||||
}
|
||||
}
|
||||
return attackResult;
|
||||
@@ -3106,7 +3241,7 @@ namespace Barotrauma
|
||||
float attackerSkillLevel = attacker.GetSkillLevel("weapons");
|
||||
attacker.Info?.IncreaseSkillLevel("weapons",
|
||||
-healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f),
|
||||
attacker.WorldPosition + Vector2.UnitY * 100.0f);
|
||||
attacker.Position + Vector2.UnitY * 100.0f);
|
||||
}
|
||||
}
|
||||
else if (healthChange > 0.0f)
|
||||
@@ -3114,7 +3249,7 @@ namespace Barotrauma
|
||||
float attackerSkillLevel = attacker.GetSkillLevel("medical");
|
||||
attacker.Info?.IncreaseSkillLevel("medical",
|
||||
healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f),
|
||||
attacker.WorldPosition + Vector2.UnitY * 100.0f);
|
||||
attacker.Position + Vector2.UnitY * 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3253,7 +3388,7 @@ namespace Barotrauma
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status });
|
||||
}
|
||||
|
||||
IsDead = true;
|
||||
isDead = true;
|
||||
|
||||
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
|
||||
|
||||
@@ -3293,9 +3428,9 @@ namespace Barotrauma
|
||||
AnimController.movement = Vector2.Zero;
|
||||
AnimController.TargetMovement = Vector2.Zero;
|
||||
|
||||
for (int i = 0; i < selectedItems.Length; i++)
|
||||
foreach (Item heldItem in HeldItems.ToList())
|
||||
{
|
||||
if (selectedItems[i] != null) selectedItems[i].Drop(this);
|
||||
heldItem.Drop(this);
|
||||
}
|
||||
|
||||
SelectedConstruction = null;
|
||||
@@ -3325,7 +3460,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
IsDead = false;
|
||||
isDead = false;
|
||||
|
||||
if (aiTarget != null)
|
||||
{
|
||||
@@ -3373,10 +3508,12 @@ namespace Barotrauma
|
||||
|
||||
base.Remove();
|
||||
|
||||
if (selectedItems[0] != null) { selectedItems[0].Drop(this); }
|
||||
if (selectedItems[1] != null) { selectedItems[1].Drop(this); }
|
||||
foreach (Item heldItem in HeldItems.ToList())
|
||||
{
|
||||
heldItem.Drop(this);
|
||||
}
|
||||
|
||||
if (info != null) { info.Remove(); }
|
||||
info?.Remove();
|
||||
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.CrewManager?.KillCharacter(this);
|
||||
@@ -3388,12 +3525,9 @@ namespace Barotrauma
|
||||
|
||||
if (Inventory != null)
|
||||
{
|
||||
foreach (Item item in Inventory.Items)
|
||||
foreach (Item item in Inventory.AllItems)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
Spawner?.AddToRemoveQueue(item);
|
||||
}
|
||||
Spawner?.AddToRemoveQueue(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3421,18 +3555,13 @@ namespace Barotrauma
|
||||
|
||||
public void SaveInventory(Inventory inventory, XElement parentElement)
|
||||
{
|
||||
var items = Array.FindAll(inventory.Items, i => i != null).Distinct();
|
||||
var items = inventory.AllItems.Distinct();
|
||||
foreach (Item item in items)
|
||||
{
|
||||
item.Submarine = inventory.Owner.Submarine;
|
||||
var itemElement = item.Save(parentElement);
|
||||
|
||||
List<int> slotIndices = new List<int>();
|
||||
for (int i = 0; i < inventory.Capacity; i++)
|
||||
{
|
||||
if (inventory.Items[i] == item) { slotIndices.Add(i); }
|
||||
}
|
||||
|
||||
List<int> slotIndices = inventory.FindIndices(item);
|
||||
itemElement.Add(new XAttribute("i", string.Join(",", slotIndices)));
|
||||
|
||||
foreach (ItemContainer container in item.GetComponents<ItemContainer>())
|
||||
@@ -3446,10 +3575,10 @@ namespace Barotrauma
|
||||
|
||||
public void SpawnInventoryItems(Inventory inventory, XElement itemData)
|
||||
{
|
||||
SpawnInventoryItemsRecursive(inventory, itemData);
|
||||
SpawnInventoryItemsRecursive(inventory, itemData, new List<Item>());
|
||||
}
|
||||
|
||||
private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element)
|
||||
private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element, List<Item> extraDuffelBags)
|
||||
{
|
||||
foreach (XElement itemElement in element.Elements())
|
||||
{
|
||||
@@ -3475,28 +3604,91 @@ namespace Barotrauma
|
||||
//this should not happen normally, but can occur if the character is accidentally given new job items while also loading previous items in the campaign
|
||||
for (int i = 0; i < inventory.Capacity; i++)
|
||||
{
|
||||
if (slotIndices.Contains(i) && inventory.Items[i] != null && inventory.Items[i] != newItem)
|
||||
if (slotIndices.Contains(i))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error while loading character inventory data. The slot {i} was already occupied by the item \"{inventory.Items[i].Name} ({inventory.Items[i].ID})\" when loading the item \"{newItem.Name} ({newItem.ID})\"");
|
||||
inventory.Items[i].Drop(null, createNetworkEvent: false);
|
||||
var existingItem = inventory.GetItemAt(i);
|
||||
if (existingItem != null && existingItem != newItem && (existingItem.prefab != newItem.prefab || existingItem.Prefab.MaxStackSize == 1))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error while loading character inventory data. The slot {i} was already occupied by the item \"{existingItem.Name} ({existingItem.ID})\" when loading the item \"{newItem.Name} ({newItem.ID})\"");
|
||||
existingItem.Drop(null, createNetworkEvent: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inventory.TryPutItem(newItem, slotIndices[0], false, false, null);
|
||||
newItem.ParentInventory = inventory;
|
||||
|
||||
//force the item to the correct slots
|
||||
// e.g. putting the item in a hand slot will also put it in the first available Any-slot,
|
||||
// which may not be where it actually was
|
||||
for (int i = 0; i < inventory.Capacity; i++)
|
||||
bool canBePutInOriginalInventory = true;
|
||||
if (slotIndices[0] >= inventory.Capacity)
|
||||
{
|
||||
if (slotIndices.Contains(i))
|
||||
canBePutInOriginalInventory = false;
|
||||
//legacy support: before item stacking was implemented, revolver for example had a separate slot for each bullet
|
||||
//now there's just one, try to put the extra items where they fit (= stack them)
|
||||
for (int i = 0; i < inventory.Capacity; i++)
|
||||
{
|
||||
inventory.Items[i] = newItem;
|
||||
if (inventory.CanBePut(newItem, i))
|
||||
{
|
||||
slotIndices[0] = i;
|
||||
canBePutInOriginalInventory = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (inventory.Items[i] == newItem)
|
||||
}
|
||||
|
||||
if (canBePutInOriginalInventory)
|
||||
{
|
||||
inventory.TryPutItem(newItem, slotIndices[0], false, false, null);
|
||||
newItem.ParentInventory = inventory;
|
||||
|
||||
//force the item to the correct slots
|
||||
// e.g. putting the item in a hand slot will also put it in the first available Any-slot,
|
||||
// which may not be where it actually was
|
||||
for (int i = 0; i < inventory.Capacity; i++)
|
||||
{
|
||||
inventory.Items[i] = null;
|
||||
if (slotIndices.Contains(i))
|
||||
{
|
||||
if (!inventory.GetItemsAt(i).Contains(newItem)) { inventory.ForceToSlot(newItem, i); }
|
||||
}
|
||||
else if (inventory.FindIndices(newItem).Contains(i))
|
||||
{
|
||||
inventory.ForceRemoveFromSlot(newItem, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// In case the inventory capacity is smaller than it was when saving:
|
||||
// 1) Spawn a new duffel bag if none yet spawned or if the existing ones aren't enough
|
||||
if (extraDuffelBags.None(i => i.OwnInventory.CanBePut(newItem)) && ItemPrefab.Find(null, "duffelbag") is ItemPrefab duffelBagPrefab)
|
||||
{
|
||||
var hull = Hull.FindHull(WorldPosition, guess: CurrentHull);
|
||||
var mainSub = Submarine.MainSubs.FirstOrDefault(s => s.TeamID == TeamID);
|
||||
if ((hull == null || hull.Submarine != mainSub) && mainSub != null)
|
||||
{
|
||||
var wp = WayPoint.GetRandom(spawnType: SpawnType.Cargo, sub: mainSub) ?? WayPoint.GetRandom(sub: mainSub);
|
||||
if (wp != null)
|
||||
{
|
||||
hull = Hull.FindHull(wp.WorldPosition);
|
||||
}
|
||||
}
|
||||
var newDuffelBag = new Item(duffelBagPrefab,
|
||||
hull != null ? CargoManager.GetCargoPos(hull, duffelBagPrefab) : Position,
|
||||
hull?.Submarine ?? Submarine);
|
||||
extraDuffelBags.Add(newDuffelBag);
|
||||
#if SERVER
|
||||
Spawner.CreateNetworkEvent(newDuffelBag, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
// 2) Find a slot for the new item
|
||||
for (int i = 0; i < extraDuffelBags.Count; i++)
|
||||
{
|
||||
var duffelBag = extraDuffelBags[i];
|
||||
for (int j = 0; j < duffelBag.OwnInventory.Capacity; j++)
|
||||
{
|
||||
if (duffelBag.OwnInventory.TryPutItem(newItem, j, false, false, null))
|
||||
{
|
||||
newItem.ParentInventory = duffelBag.OwnInventory;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3506,7 +3698,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (itemContainerIndex >= itemContainers.Count) break;
|
||||
if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement);
|
||||
SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement, extraDuffelBags);
|
||||
itemContainerIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +345,7 @@ namespace Barotrauma
|
||||
|
||||
public CauseOfDeath CauseOfDeath;
|
||||
|
||||
public Character.TeamType TeamID;
|
||||
public CharacterTeamType TeamID;
|
||||
|
||||
private readonly NPCPersonalityTrait personalityTrait;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
+28
-9
@@ -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)
|
||||
@@ -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));
|
||||
@@ -154,6 +170,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
//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)) { return; }
|
||||
|
||||
//create the AI husk in a coroutine to ensure that we don't modify the character list while enumerating it
|
||||
CoroutineManager.StartCoroutine(CreateAIHusk());
|
||||
}
|
||||
@@ -179,7 +199,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 +221,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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -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,6 +105,10 @@ 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
|
||||
@@ -572,7 +580,7 @@ namespace Barotrauma
|
||||
|
||||
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));
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -330,7 +330,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public Submarine Submarine => character.Submarine;
|
||||
public Submarine Submarine => character?.Submarine;
|
||||
|
||||
public bool Hidden
|
||||
{
|
||||
@@ -340,7 +340,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 +622,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 +677,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 +717,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
float finalDamageModifier = 1.0f;
|
||||
float finalDamageModifier = damageMultiplier;
|
||||
foreach (DamageModifier damageModifier in tempModifiers)
|
||||
{
|
||||
finalDamageModifier *= damageModifier.DamageMultiplier;
|
||||
|
||||
+20
-7
@@ -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!
|
||||
|
||||
+8
-8
@@ -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>();
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
@@ -78,6 +78,8 @@ namespace Barotrauma
|
||||
|
||||
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 +102,32 @@ 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())
|
||||
{
|
||||
matchingParams = matchingParams.SubParams.FirstOrDefault(p => p.Name.Equals(subSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase));
|
||||
if (matchingParams != null)
|
||||
{
|
||||
TryLoadOverride(matchingParams, subSubElement, matchingParams.SerializableProperties);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
if (string.IsNullOrEmpty(SpeciesName) && MainElement != null)
|
||||
{
|
||||
//backwards compatibility
|
||||
@@ -111,6 +139,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 +211,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))
|
||||
{
|
||||
@@ -486,17 +528,21 @@ namespace Barotrauma
|
||||
[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()]
|
||||
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.")]
|
||||
public bool Infiltrate { 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,10 +634,13 @@ 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)]
|
||||
public bool IgnoreIncapacitated { get; set; }
|
||||
|
||||
[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; }
|
||||
@@ -602,6 +651,9 @@ 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; }
|
||||
|
||||
[Serialize(0f, true, description: "How much damage the protected target should take from an attacker before the creature starts defending it.")]
|
||||
public float Threshold { get; private set; }
|
||||
|
||||
public TargetParams(XElement element, CharacterParams character) : base(element, character) { }
|
||||
|
||||
public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { }
|
||||
|
||||
@@ -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!");
|
||||
|
||||
+17
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -832,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));
|
||||
|
||||
@@ -1297,6 +1299,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (item.CurrentHull != null && item.HasTag("ballast") && item.GetComponent<Pump>() is { } pump)
|
||||
{
|
||||
if (item.CurrentHull.BallastFlora != null) { continue; }
|
||||
pumps.Add(pump);
|
||||
}
|
||||
}
|
||||
@@ -1311,8 +1314,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;
|
||||
}
|
||||
|
||||
@@ -1458,6 +1461,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, () =>
|
||||
@@ -1468,6 +1493,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));
|
||||
@@ -1765,7 +1791,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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -104,19 +111,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,7 +185,7 @@ 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)
|
||||
{
|
||||
@@ -255,7 +269,12 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Options.Any())
|
||||
if (ShouldInterrupt())
|
||||
{
|
||||
ResetSpeaker();
|
||||
interrupt = true;
|
||||
}
|
||||
else if (Options.Any())
|
||||
{
|
||||
Options[selectedOption].Update(deltaTime);
|
||||
}
|
||||
@@ -335,6 +354,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;
|
||||
}
|
||||
|
||||
@@ -109,14 +109,14 @@ 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; }
|
||||
item.SpawnedInOutpost = true;
|
||||
}
|
||||
}
|
||||
newCharacter.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier;
|
||||
@@ -200,12 +200,18 @@ 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;
|
||||
newItem?.SetIgnoreByAI(IgnoreByAI);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>>();
|
||||
@@ -144,6 +151,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 +268,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 +342,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++)
|
||||
{
|
||||
@@ -358,6 +383,7 @@ namespace Barotrauma
|
||||
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))
|
||||
{
|
||||
@@ -442,6 +468,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)
|
||||
{
|
||||
@@ -493,6 +527,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)
|
||||
{
|
||||
@@ -500,9 +553,6 @@ namespace Barotrauma
|
||||
calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval;
|
||||
}
|
||||
|
||||
eventThreshold += settings.EventThresholdIncrease * deltaTime;
|
||||
eventCoolDown -= deltaTime;
|
||||
|
||||
if (currentIntensity < eventThreshold)
|
||||
{
|
||||
bool recheck = false;
|
||||
@@ -526,7 +576,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,7 +616,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);
|
||||
@@ -586,9 +639,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)))
|
||||
{
|
||||
@@ -681,7 +733,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Finds all actions in a ScriptedEvent
|
||||
/// </summary>
|
||||
@@ -750,5 +801,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace Barotrauma
|
||||
public readonly Type EventType;
|
||||
public readonly string MusicType;
|
||||
public readonly float SpawnProbability;
|
||||
public readonly bool TriggerEventCooldown;
|
||||
public float Commonness;
|
||||
public string Identifier;
|
||||
|
||||
@@ -35,6 +36,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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,7 +86,7 @@ namespace Barotrauma
|
||||
public bool IsInWinningTeam(Character character)
|
||||
{
|
||||
return character != null &&
|
||||
Winner != Character.TeamType.None &&
|
||||
Winner != CharacterTeamType.None &&
|
||||
Winner == character.TeamID;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -122,7 +117,7 @@ namespace Barotrauma
|
||||
{
|
||||
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
|
||||
@@ -74,6 +76,8 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
caves.Clear();
|
||||
|
||||
if (IsClient) { return; }
|
||||
foreach (var kvp in ResourceClusters)
|
||||
{
|
||||
@@ -93,6 +97,19 @@ namespace Barotrauma
|
||||
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();
|
||||
|
||||
@@ -85,11 +85,6 @@ namespace Barotrauma
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public virtual int TeamCount
|
||||
{
|
||||
get { return 1; }
|
||||
}
|
||||
|
||||
public virtual IEnumerable<Vector2> SonarPositions
|
||||
{
|
||||
get { return Enumerable.Empty<Vector2>(); }
|
||||
@@ -184,11 +179,6 @@ namespace Barotrauma
|
||||
|
||||
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);
|
||||
@@ -238,7 +228,16 @@ namespace Barotrauma
|
||||
{
|
||||
if (GameMain.GameSession.GameMode is CampaignMode && !IsClient)
|
||||
{
|
||||
int srcIndex = Locations[0].Type.Identifier.Equals(from, StringComparison.OrdinalIgnoreCase) ? 0 : 1;
|
||||
int srcIndex = -1;
|
||||
for (int i = 0; i < Locations.Length; i++)
|
||||
{
|
||||
if (Locations[i].Type.Identifier.Equals(from, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
srcIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (srcIndex == -1) { return; }
|
||||
var upgradeLocation = Locations[srcIndex];
|
||||
upgradeLocation.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(to, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
@@ -125,7 +125,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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -53,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", "");
|
||||
@@ -62,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);
|
||||
@@ -107,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())
|
||||
{
|
||||
@@ -188,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)
|
||||
@@ -279,6 +307,10 @@ namespace Barotrauma
|
||||
{
|
||||
GiveReward();
|
||||
completed = true;
|
||||
if (completed)
|
||||
{
|
||||
ChangeLocationType("None", "Explored");
|
||||
}
|
||||
}
|
||||
foreach (Item item in items)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,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)
|
||||
@@ -180,33 +185,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 (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 +256,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 +270,20 @@ 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++)
|
||||
{
|
||||
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 +352,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -196,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,11 +140,35 @@ 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()
|
||||
{
|
||||
@@ -172,73 +200,59 @@ namespace Barotrauma
|
||||
#else
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
ChatMessage msg = ChatMessage.Create("", $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}", ChatMessageType.ServerMessageBoxInGame, null);
|
||||
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 +267,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 +290,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)
|
||||
@@ -124,12 +147,14 @@ namespace Barotrauma
|
||||
AddCharacterToCrewList(character);
|
||||
AddCurrentOrderIcon(character, character.CurrentOrder, character.CurrentOrderOption);
|
||||
#endif
|
||||
var idleObjective = character.AIController?.ObjectiveManager?.GetObjective<AIObjectiveIdle>();
|
||||
if (idleObjective != null)
|
||||
if (character.AIController is HumanAIController humanAI)
|
||||
{
|
||||
idleObjective.Behavior = character.Info.Job.Prefab.IdleBehavior;
|
||||
}
|
||||
|
||||
var idleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
|
||||
if (idleObjective != null)
|
||||
{
|
||||
idleObjective.Behavior = character.Info.Job.Prefab.IdleBehavior;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddCharacterInfo(CharacterInfo characterInfo)
|
||||
@@ -177,7 +202,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)
|
||||
{
|
||||
@@ -262,8 +287,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;
|
||||
@@ -700,7 +699,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)
|
||||
|
||||
+6
-1
@@ -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,4 +1,7 @@
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -7,5 +10,42 @@ namespace Barotrauma
|
||||
public PvPMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Barotrauma
|
||||
|
||||
public Mission Mission { get; private set; }
|
||||
|
||||
public Character.TeamType? WinningTeam;
|
||||
public CharacterTeamType? WinningTeam;
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
@@ -107,7 +107,7 @@ 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>
|
||||
@@ -117,7 +117,7 @@ namespace Barotrauma
|
||||
: this(submarineInfo)
|
||||
{
|
||||
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
|
||||
GameMode = InstantiateGameMode(gameModePreset, seed, missionPrefab: missionPrefab);
|
||||
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionPrefab: missionPrefab);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -158,7 +158,7 @@ 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, MissionPrefab missionPrefab = null, MissionType missionType = MissionType.None)
|
||||
{
|
||||
if (gameModePreset.GameModeType == typeof(CoOpMode))
|
||||
{
|
||||
@@ -174,12 +174,22 @@ namespace Barotrauma
|
||||
}
|
||||
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))
|
||||
{
|
||||
@@ -230,7 +240,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 +262,7 @@ namespace Barotrauma
|
||||
Campaign.Money -= cost;
|
||||
|
||||
((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine;
|
||||
return newSubmarine;
|
||||
}
|
||||
|
||||
public void PurchaseSubmarine(SubmarineInfo newSubmarine)
|
||||
@@ -306,7 +317,7 @@ namespace Barotrauma
|
||||
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 +327,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);
|
||||
}
|
||||
@@ -438,6 +449,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
GameMain.Config.RecentlyEncounteredCreatures.Clear();
|
||||
|
||||
GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition;
|
||||
RoundStartTime = Timing.TotalTime;
|
||||
GameMain.ResetFrameTime();
|
||||
@@ -573,7 +586,7 @@ 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);
|
||||
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.IO;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Diagnostics;
|
||||
#if CLIENT
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
@@ -50,6 +51,7 @@ namespace Barotrauma
|
||||
public bool DynamicRangeCompressionEnabled { get; set; }
|
||||
public bool VoipAttenuationEnabled { get; set; }
|
||||
public bool UseDirectionalVoiceChat { get; set; }
|
||||
public bool DisableVoiceChatFilters { get; set; }
|
||||
|
||||
public IList<string> AudioDeviceNames;
|
||||
public IList<string> CaptureDeviceNames;
|
||||
@@ -68,9 +70,12 @@ namespace Barotrauma
|
||||
|
||||
public float NoiseGateThreshold { get; set; }
|
||||
|
||||
public bool UseLocalVoiceByDefault { get; set; }
|
||||
|
||||
#if CLIENT
|
||||
private KeyOrMouse[] keyMapping;
|
||||
public KeyOrMouse[] keyMapping;
|
||||
private KeyOrMouse[] inventoryKeyMapping;
|
||||
public static Dictionary<Keys, string> ConsoleKeybinds = new Dictionary<Keys, string>();
|
||||
#endif
|
||||
|
||||
private WindowMode windowMode;
|
||||
@@ -123,6 +128,8 @@ namespace Barotrauma
|
||||
set { jobPreferences = value; }
|
||||
}
|
||||
|
||||
public CharacterTeamType TeamPreference { get; set; }
|
||||
|
||||
public bool AreJobPreferencesEqual(List<Pair<string, int>> compareTo)
|
||||
{
|
||||
if (jobPreferences == null || compareTo == null) return false;
|
||||
@@ -154,6 +161,8 @@ namespace Barotrauma
|
||||
|
||||
public bool EnableMouseLook { get; set; } = true;
|
||||
|
||||
public bool EnableRadialDistortion { get; set; } = true;
|
||||
|
||||
public bool CrewMenuOpen { get; set; } = true;
|
||||
public bool ChatOpen { get; set; } = true;
|
||||
|
||||
@@ -282,6 +291,12 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
public XElement ServerFilterElement
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public volatile bool SuppressModFolderWatcher;
|
||||
|
||||
public volatile bool WaitingForAutoUpdate;
|
||||
@@ -349,22 +364,28 @@ namespace Barotrauma
|
||||
|
||||
private List<Tuple<ContentPackage, bool>> backupModOrder;
|
||||
|
||||
public void SwapPackages(ContentPackage corePackage, List<ContentPackage> regularPackages)
|
||||
public void BackUpModOrder()
|
||||
{
|
||||
backupModOrder = new List<Tuple<ContentPackage, bool>>();
|
||||
backupModOrder.Add(new Tuple<ContentPackage, bool>(CurrentCorePackage, true));
|
||||
for (int i=0;i<ContentPackage.RegularPackages.Count;i++)
|
||||
backupModOrder = new List<Tuple<ContentPackage, bool>>
|
||||
{
|
||||
new Tuple<ContentPackage, bool>(CurrentCorePackage, true)
|
||||
};
|
||||
for (int i = 0; i < ContentPackage.RegularPackages.Count; i++)
|
||||
{
|
||||
var p = ContentPackage.RegularPackages[i];
|
||||
backupModOrder.Add(new Tuple<ContentPackage, bool>(p, EnabledRegularPackages.Contains(p)));
|
||||
}
|
||||
}
|
||||
|
||||
public void SwapPackages(ContentPackage corePackage, List<ContentPackage> regularPackages)
|
||||
{
|
||||
List<ContentPackage> packagesToDisable = new List<ContentPackage>();
|
||||
packagesToDisable.Add(CurrentCorePackage);
|
||||
packagesToDisable.AddRange(EnabledRegularPackages.Where(p => p.HasMultiplayerIncompatibleContent));
|
||||
packagesToDisable.AddRange(enabledRegularPackages.Where(p => p.HasMultiplayerIncompatibleContent));
|
||||
List<ContentPackage> packagesToEnable = new List<ContentPackage>();
|
||||
packagesToEnable.Add(corePackage);
|
||||
packagesToEnable.AddRange(regularPackages);
|
||||
List<ContentPackage> regularPackagesToAdd = regularPackages.Where(p => p.HasMultiplayerIncompatibleContent).ToList();
|
||||
packagesToEnable.AddRange(regularPackagesToAdd);
|
||||
|
||||
IEnumerable<ContentFile> filesOfDisabledPkgs = packagesToDisable.SelectMany(p => p.Files);
|
||||
IEnumerable<ContentFile> filesOfEnabledPkgs = packagesToEnable.SelectMany(p => p.Files);
|
||||
@@ -378,14 +399,18 @@ namespace Barotrauma
|
||||
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
|
||||
|
||||
CurrentCorePackage = corePackage;
|
||||
enabledRegularPackages.RemoveAll(p => p.HasMultiplayerIncompatibleContent); enabledRegularPackages.AddRange(regularPackages);
|
||||
enabledRegularPackages.RemoveAll(p => p.HasMultiplayerIncompatibleContent); enabledRegularPackages.AddRange(regularPackagesToAdd);
|
||||
|
||||
DisableContentPackageItems(filesToDisable);
|
||||
EnableContentPackageItems(filesToEnable);
|
||||
|
||||
RefreshContentPackageItems(filesOfEnabledPkgs.Concat(filesToDisable));
|
||||
|
||||
ContentPackage.SortContentPackages(p => -regularPackages.IndexOf(p));
|
||||
ContentPackage.SortContentPackages(p => -regularPackages.IndexOf(p), config: this);
|
||||
|
||||
#if DEBUG
|
||||
Debug.Assert(enabledRegularPackages.Count == enabledRegularPackages.Distinct().Count());
|
||||
#endif
|
||||
}
|
||||
|
||||
public void RestoreBackupPackages()
|
||||
@@ -395,7 +420,7 @@ namespace Barotrauma
|
||||
SwapPackages(
|
||||
backupModOrder[0].Item1,
|
||||
backupModOrder.Skip(1).Where(p => p.Item2).Select(p => p.Item1).ToList());
|
||||
ContentPackage.SortContentPackages(p => backupModOrder.FindIndex(n => n.Item1 == p));
|
||||
ContentPackage.SortContentPackages(p => backupModOrder.FindIndex(n => n.Item1 == p), config: this);
|
||||
|
||||
backupModOrder = null;
|
||||
}
|
||||
@@ -428,6 +453,8 @@ namespace Barotrauma
|
||||
|
||||
public void SortContentPackages(bool refreshAll = false)
|
||||
{
|
||||
var previousEnabledRegularPackages = enabledRegularPackages.ToList();
|
||||
|
||||
for (int i = enabledRegularPackages.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var package = enabledRegularPackages[i];
|
||||
@@ -465,6 +492,7 @@ namespace Barotrauma
|
||||
var sortedSelected = enabledRegularPackages
|
||||
.OrderBy(p => -ContentPackage.RegularPackages.IndexOf(p))
|
||||
.ToList();
|
||||
if (previousEnabledRegularPackages.SequenceEqual(sortedSelected)) { return; }
|
||||
enabledRegularPackages.Clear(); enabledRegularPackages.AddRange(sortedSelected);
|
||||
|
||||
CharacterPrefab.Prefabs.SortAll();
|
||||
@@ -698,10 +726,19 @@ namespace Barotrauma
|
||||
|
||||
private const float MinHUDScale = 0.75f, MaxHUDScale = 1.25f;
|
||||
public static float HUDScale { get; set; }
|
||||
|
||||
private const float MinInventoryScale = 0.75f, MaxInventoryScale = 1.25f;
|
||||
public static float InventoryScale { get; set; }
|
||||
|
||||
private const float MinTextScale = 0.5f, MaxTextScale = 1.5f;
|
||||
public static float TextScale { get; set; }
|
||||
private bool textScaleDirty;
|
||||
|
||||
public List<string> CompletedTutorialNames { get; private set; }
|
||||
public HashSet<string> EncounteredCreatures { get; private set; } = new HashSet<string>();
|
||||
public HashSet<string> KilledCreatures { get; private set; } = new HashSet<string>();
|
||||
|
||||
public readonly HashSet<string> RecentlyEncounteredCreatures = new HashSet<string>();
|
||||
|
||||
public static bool VerboseLogging { get; set; }
|
||||
public static bool SaveDebugConsoleLogs { get; set; }
|
||||
@@ -729,10 +766,13 @@ namespace Barotrauma
|
||||
|
||||
public bool ShowLanguageSelectionPrompt { get; set; }
|
||||
|
||||
public static bool ShowOffensiveServerPrompt { get; set; }
|
||||
|
||||
private bool showTutorialSkipWarning = true;
|
||||
|
||||
public static bool EnableSubmarineAutoSave { get; set; }
|
||||
public static int MaximumAutoSaves { get; set; }
|
||||
public static int AutoSaveIntervalSeconds { get; set; }
|
||||
public static Color SubEditorBackgroundColor { get; set; }
|
||||
public static int SubEditorMaxUndoBuffer { get; set; }
|
||||
|
||||
@@ -778,7 +818,8 @@ namespace Barotrauma
|
||||
if (File.Exists(cpPath) &&
|
||||
!ContentPackage.AllPackages.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath))
|
||||
{
|
||||
ContentPackage.AddPackage(new ContentPackage(cpPath));
|
||||
var newPackage = new ContentPackage(cpPath);
|
||||
if (!newPackage.IsCorrupt) { ContentPackage.AddPackage(newPackage); }
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -830,7 +871,8 @@ namespace Barotrauma
|
||||
if (File.Exists(cpPath) &&
|
||||
!ContentPackage.AllPackages.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath))
|
||||
{
|
||||
ContentPackage.AddPackage(new ContentPackage(cpPath));
|
||||
var newPackage = new ContentPackage(cpPath);
|
||||
if (!newPackage.IsCorrupt) { ContentPackage.AddPackage(newPackage); }
|
||||
}
|
||||
if (reselectCore) { AutoSelectCorePackage(null); }
|
||||
}
|
||||
@@ -869,6 +911,7 @@ namespace Barotrauma
|
||||
LoadAudioSettings(doc);
|
||||
#if CLIENT
|
||||
LoadControls(doc);
|
||||
LoadSubEditorImages(doc);
|
||||
#endif
|
||||
if (loadContentPackages)
|
||||
{
|
||||
@@ -905,6 +948,7 @@ namespace Barotrauma
|
||||
new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs),
|
||||
new XAttribute("submarineautosave", EnableSubmarineAutoSave),
|
||||
new XAttribute("maxautosaves", MaximumAutoSaves),
|
||||
new XAttribute("autosaveintervalseconds", AutoSaveIntervalSeconds),
|
||||
new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)),
|
||||
new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer),
|
||||
new XAttribute("enablesplashscreen", EnableSplashScreen),
|
||||
@@ -1010,6 +1054,11 @@ namespace Barotrauma
|
||||
jobPreferences.Add(jobElement);
|
||||
}
|
||||
gameplay.Add(jobPreferences);
|
||||
|
||||
var teamPreference = new XElement("teampreference");
|
||||
teamPreference.Add(new XAttribute("team", TeamPreference.ToString()));
|
||||
gameplay.Add(teamPreference);
|
||||
|
||||
doc.Root.Add(gameplay);
|
||||
|
||||
var playerElement = new XElement("player",
|
||||
@@ -1079,6 +1128,7 @@ namespace Barotrauma
|
||||
LoadAudioSettings(doc);
|
||||
#if CLIENT
|
||||
LoadControls(doc);
|
||||
LoadSubEditorImages(doc);
|
||||
#endif
|
||||
LoadContentPackages(doc);
|
||||
|
||||
@@ -1101,8 +1151,21 @@ namespace Barotrauma
|
||||
CompletedTutorialNames.Add(element.GetAttributeString("name", ""));
|
||||
}
|
||||
}
|
||||
XElement encounters = doc.Root.Element("encountered");
|
||||
if (encounters != null)
|
||||
{
|
||||
EncounteredCreatures = new HashSet<string>(encounters.GetAttributeStringArray("creatures", new string[0], convertToLowerInvariant: true));
|
||||
}
|
||||
XElement kills = doc.Root.Element("killed");
|
||||
if (kills != null)
|
||||
{
|
||||
KilledCreatures = new HashSet<string>(kills.GetAttributeStringArray("creatures", new string[0], convertToLowerInvariant: true));
|
||||
}
|
||||
|
||||
ServerFilterElement = doc.Root.Element("serverfilters");
|
||||
|
||||
UnsavedSettings = false;
|
||||
textScaleDirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1130,6 +1193,7 @@ namespace Barotrauma
|
||||
new XAttribute("submarineautosave", EnableSubmarineAutoSave),
|
||||
new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer),
|
||||
new XAttribute("maxautosaves", MaximumAutoSaves),
|
||||
new XAttribute("autosaveintervalseconds", AutoSaveIntervalSeconds),
|
||||
new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)),
|
||||
new XAttribute("enablesplashscreen", EnableSplashScreen),
|
||||
new XAttribute("usesteammatchmaking", UseSteamMatchmaking),
|
||||
@@ -1139,6 +1203,7 @@ namespace Barotrauma
|
||||
new XAttribute("pauseonfocuslost", PauseOnFocusLost),
|
||||
new XAttribute("aimassistamount", aimAssistAmount),
|
||||
new XAttribute("enablemouselook", EnableMouseLook),
|
||||
new XAttribute("radialdistortion", EnableRadialDistortion),
|
||||
new XAttribute("chatopen", ChatOpen),
|
||||
new XAttribute("crewmenuopen", CrewMenuOpen),
|
||||
new XAttribute("campaigndisclaimershown", CampaignDisclaimerShown),
|
||||
@@ -1209,7 +1274,8 @@ namespace Barotrauma
|
||||
new XAttribute("voicesetting", VoiceSetting),
|
||||
new XAttribute("audiooutputdevice", System.Xml.XmlConvert.EncodeName(AudioOutputDevice ?? "")),
|
||||
new XAttribute("voicecapturedevice", System.Xml.XmlConvert.EncodeName(VoiceCaptureDevice ?? "")),
|
||||
new XAttribute("noisegatethreshold", NoiseGateThreshold));
|
||||
new XAttribute("noisegatethreshold", NoiseGateThreshold),
|
||||
new XAttribute("uselocalvoicebydefault", UseLocalVoiceByDefault));
|
||||
|
||||
XElement gSettings = doc.Root.Element("graphicssettings");
|
||||
if (gSettings == null)
|
||||
@@ -1224,7 +1290,8 @@ namespace Barotrauma
|
||||
new XAttribute("chromaticaberration", ChromaticAberrationEnabled),
|
||||
new XAttribute("losmode", LosMode),
|
||||
new XAttribute("hudscale", HUDScale),
|
||||
new XAttribute("inventoryscale", InventoryScale));
|
||||
new XAttribute("inventoryscale", InventoryScale),
|
||||
new XAttribute("textscale", TextScale));
|
||||
|
||||
XElement contentPackagesElement = new XElement("contentpackages");
|
||||
|
||||
@@ -1273,6 +1340,25 @@ namespace Barotrauma
|
||||
inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton));
|
||||
}
|
||||
}
|
||||
|
||||
var debugconsoleKeyMappingElement = new XElement("debugconsolemapping");
|
||||
doc.Root.Add(debugconsoleKeyMappingElement);
|
||||
foreach (var (key, command) in ConsoleKeybinds)
|
||||
{
|
||||
debugconsoleKeyMappingElement.Add(new XElement("Keybind",
|
||||
new XAttribute("key", key.ToString()),
|
||||
new XAttribute("command", command)));
|
||||
}
|
||||
|
||||
if (ServerFilterElement == null)
|
||||
{
|
||||
ShowOffensiveServerPrompt = true;
|
||||
ServerFilterElement = new XElement("serverfilters");
|
||||
}
|
||||
GameMain.ServerListScreen?.SaveServerFilters(ServerFilterElement);
|
||||
doc.Root.Add(ServerFilterElement);
|
||||
|
||||
SubEditorScreen.ImageManager.Save(doc.Root);
|
||||
#endif
|
||||
|
||||
var gameplay = new XElement("gameplay");
|
||||
@@ -1317,6 +1403,9 @@ namespace Barotrauma
|
||||
}
|
||||
doc.Root.Add(tutorialElement);
|
||||
|
||||
doc.Root.Add(new XElement("encountered", new XAttribute("creatures", string.Join(",", EncounteredCreatures).Trim().ToLowerInvariant())));
|
||||
doc.Root.Add(new XElement("killed", new XAttribute("creatures", string.Join(",", KilledCreatures).Trim().ToLowerInvariant())));
|
||||
|
||||
System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
@@ -1353,6 +1442,7 @@ namespace Barotrauma
|
||||
QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName);
|
||||
EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true);
|
||||
MaximumAutoSaves = doc.Root.GetAttributeInt("maxautosaves", 8);
|
||||
AutoSaveIntervalSeconds = doc.Root.GetAttributeInt("autosaveintervalseconds", 300);
|
||||
SubEditorBackgroundColor = doc.Root.GetAttributeColor("subeditorbackground", new Color(0.051f, 0.149f, 0.271f, 1.0f));
|
||||
SubEditorMaxUndoBuffer = doc.Root.GetAttributeInt("subeditorundobuffer", 32);
|
||||
UseSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", UseSteamMatchmaking);
|
||||
@@ -1361,6 +1451,7 @@ namespace Barotrauma
|
||||
PauseOnFocusLost = doc.Root.GetAttributeBool("pauseonfocuslost", PauseOnFocusLost);
|
||||
AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", AimAssistAmount);
|
||||
EnableMouseLook = doc.Root.GetAttributeBool("enablemouselook", EnableMouseLook);
|
||||
EnableRadialDistortion = doc.Root.GetAttributeBool("radialdistortion", EnableRadialDistortion);
|
||||
CrewMenuOpen = doc.Root.GetAttributeBool("crewmenuopen", CrewMenuOpen);
|
||||
ChatOpen = doc.Root.GetAttributeBool("chatopen", ChatOpen);
|
||||
CorpseDespawnDelay = doc.Root.GetAttributeInt("corpsedespawndelay", 10 * 60);
|
||||
@@ -1389,6 +1480,12 @@ namespace Barotrauma
|
||||
jobPreferences.Add(new Pair<string, int>(jobIdentifier, outfitVariant));
|
||||
}
|
||||
}
|
||||
|
||||
var teamPreferenceElement = gameplayElement.Element("teampreference");
|
||||
if (teamPreferenceElement != null)
|
||||
{
|
||||
TeamPreference = (CharacterTeamType)Enum.Parse(typeof(CharacterTeamType), teamPreferenceElement.GetAttributeString("team", CharacterTeamType.None.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
XElement playerElement = doc.Root.Element("player");
|
||||
@@ -1430,6 +1527,7 @@ namespace Barotrauma
|
||||
ChromaticAberrationEnabled = graphicsSettings.GetAttributeBool("chromaticaberration", ChromaticAberrationEnabled);
|
||||
HUDScale = graphicsSettings.GetAttributeFloat("hudscale", HUDScale);
|
||||
InventoryScale = graphicsSettings.GetAttributeFloat("inventoryscale", InventoryScale);
|
||||
TextScale = graphicsSettings.GetAttributeFloat("textscale", TextScale);
|
||||
var losModeStr = graphicsSettings.GetAttributeString("losmode", "Transparent");
|
||||
if (!Enum.TryParse(losModeStr, out losMode))
|
||||
{
|
||||
@@ -1466,6 +1564,7 @@ namespace Barotrauma
|
||||
VoiceCaptureDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice));
|
||||
AudioOutputDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("audiooutputdevice", AudioOutputDevice));
|
||||
NoiseGateThreshold = audioSettings.GetAttributeFloat("noisegatethreshold", NoiseGateThreshold);
|
||||
UseLocalVoiceByDefault = audioSettings.GetAttributeBool("uselocalvoicebydefault", UseLocalVoiceByDefault);
|
||||
MicrophoneVolume = audioSettings.GetAttributeFloat("microphonevolume", MicrophoneVolume);
|
||||
string voiceSettingStr = audioSettings.GetAttributeString("voicesetting", "");
|
||||
if (Enum.TryParse(voiceSettingStr, out VoiceMode voiceSetting))
|
||||
@@ -1495,16 +1594,6 @@ namespace Barotrauma
|
||||
List<XElement> subElements = regularElement?.Elements()?.ToList();
|
||||
if (subElements != null)
|
||||
{
|
||||
ContentPackage.SortContentPackages(p =>
|
||||
{
|
||||
int index = subElements.FindIndex(e =>
|
||||
{
|
||||
string name = e.GetAttributeString("name", null);
|
||||
return p.Name.Equals(name, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
return index;
|
||||
});
|
||||
|
||||
foreach (var subElement in subElements)
|
||||
{
|
||||
if (!bool.TryParse(subElement.GetAttributeString("enabled", "false"), out bool enabled) || !enabled) { continue; }
|
||||
@@ -1516,6 +1605,16 @@ namespace Barotrauma
|
||||
if (package == null) { continue; }
|
||||
enabledRegularPackages.Add(package);
|
||||
}
|
||||
|
||||
ContentPackage.SortContentPackages(p =>
|
||||
{
|
||||
int index = subElements.FindIndex(e =>
|
||||
{
|
||||
string name = e.GetAttributeString("name", null);
|
||||
return p.Name.Equals(name, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
return index;
|
||||
}, config: this);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1532,8 +1631,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
ContentPackage.SortContentPackages(p => enabledContentPackagePaths.IndexOf(p.Path.CleanUpPath().ToLowerInvariant()));
|
||||
|
||||
foreach (string path in enabledContentPackagePaths)
|
||||
{
|
||||
ContentPackage package = ContentPackage.AllPackages
|
||||
@@ -1542,6 +1639,8 @@ namespace Barotrauma
|
||||
if (package.IsCorePackage) { CurrentCorePackage = package; }
|
||||
else { enabledRegularPackages.Add(package); }
|
||||
}
|
||||
|
||||
ContentPackage.SortContentPackages(p => enabledContentPackagePaths.IndexOf(p.Path.CleanUpPath().ToLowerInvariant()), config: this);
|
||||
}
|
||||
|
||||
if (CurrentCorePackage == null)
|
||||
@@ -1583,6 +1682,7 @@ namespace Barotrauma
|
||||
VoiceSetting = VoiceMode.Disabled;
|
||||
VoiceCaptureDevice = null;
|
||||
NoiseGateThreshold = -45;
|
||||
UseLocalVoiceByDefault = false;
|
||||
windowMode = WindowMode.BorderlessWindowed;
|
||||
losMode = LosMode.Transparent;
|
||||
UseSteamMatchmaking = true;
|
||||
@@ -1597,6 +1697,7 @@ namespace Barotrauma
|
||||
CharacterRace = Race.White;
|
||||
aimAssistAmount = 0.5f;
|
||||
EnableMouseLook = true;
|
||||
EnableRadialDistortion = true;
|
||||
CrewMenuOpen = true;
|
||||
ChatOpen = true;
|
||||
soundVolume = 0.5f;
|
||||
@@ -1622,6 +1723,8 @@ namespace Barotrauma
|
||||
VerboseLogging = false;
|
||||
SaveDebugConsoleLogs = false;
|
||||
AutoUpdateWorkshopItems = true;
|
||||
TextScale = 1;
|
||||
textScaleDirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ namespace Barotrauma
|
||||
Deselect,
|
||||
Shoot,
|
||||
Command,
|
||||
ToggleInventory,
|
||||
ToggleInventory,
|
||||
TakeOneFromInventorySlot,
|
||||
TakeHalfFromInventorySlot,
|
||||
NextFireMode,
|
||||
PreviousFireMode
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Barotrauma
|
||||
|
||||
partial class CharacterInventory : Inventory
|
||||
{
|
||||
private Character character;
|
||||
private readonly Character character;
|
||||
|
||||
public InvSlotType[] SlotTypes
|
||||
{
|
||||
@@ -33,8 +33,14 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
private static string[] ParseSlotTypes(XElement element)
|
||||
{
|
||||
string slotString = element.GetAttributeString("slots", null);
|
||||
return slotString == null ? new string[0] : slotString.Split(',');
|
||||
}
|
||||
|
||||
public CharacterInventory(XElement element, Character character)
|
||||
: base(character, element.GetAttributeString("slots", "").Split(',').Count())
|
||||
: base(character, ParseSlotTypes(element).Length)
|
||||
{
|
||||
this.character = character;
|
||||
IsEquipped = new bool[capacity];
|
||||
@@ -42,7 +48,7 @@ namespace Barotrauma
|
||||
|
||||
AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", true);
|
||||
|
||||
string[] slotTypeNames = element.GetAttributeString("slots", "").Split(',');
|
||||
string[] slotTypeNames = ParseSlotTypes(element);
|
||||
System.Diagnostics.Debug.Assert(slotTypeNames.Length == capacity);
|
||||
|
||||
for (int i = 0; i < capacity; i++)
|
||||
@@ -56,11 +62,9 @@ namespace Barotrauma
|
||||
SlotTypes[i] = parsedSlotType;
|
||||
switch (SlotTypes[i])
|
||||
{
|
||||
//case InvSlotType.Head:
|
||||
//case InvSlotType.OuterClothes:
|
||||
case InvSlotType.LeftHand:
|
||||
case InvSlotType.RightHand:
|
||||
hideEmptySlot[i] = true;
|
||||
slots[i].HideIfEmpty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -83,7 +87,7 @@ namespace Barotrauma
|
||||
continue;
|
||||
}
|
||||
|
||||
Entity.Spawner?.AddToSpawnQueue(itemPrefab, this);
|
||||
Entity.Spawner?.AddToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,18 +95,28 @@ namespace Barotrauma
|
||||
|
||||
public int FindLimbSlot(InvSlotType limbSlot)
|
||||
{
|
||||
for (int i = 0; i < Items.Length; i++)
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
if (SlotTypes[i] == limbSlot) { return i; }
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public Item GetItemInLimbSlot(InvSlotType limbSlot)
|
||||
{
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
if (SlotTypes[i] == limbSlot) { return slots[i].FirstOrDefault(); }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public bool IsInLimbSlot(Item item, InvSlotType limbSlot)
|
||||
{
|
||||
for (int i = 0; i < Items.Length; i++)
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
if (Items[i] == item && SlotTypes[i] == limbSlot) { return true; }
|
||||
if (SlotTypes[i] == limbSlot && slots[i].Contains(item)) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -118,9 +132,9 @@ namespace Barotrauma
|
||||
foreach (var allowedSlot in item.AllowedSlots)
|
||||
{
|
||||
InvSlotType slotsFree = InvSlotType.None;
|
||||
for (int i = 0; i < Items.Length; i++)
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
if (allowedSlot.HasFlag(SlotTypes[i]) && Items[i] == null) { slotsFree |= SlotTypes[i]; }
|
||||
if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Empty()) { slotsFree |= SlotTypes[i]; }
|
||||
}
|
||||
if (allowedSlot == slotsFree) { return true; }
|
||||
}
|
||||
@@ -130,7 +144,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// If there is no room in the generic inventory (InvSlotType.Any), check if the item can be auto-equipped into its respective limbslot
|
||||
/// </summary>
|
||||
public bool TryPutItemWithAutoEquipCheck(Item item, Character user, List<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
|
||||
public bool TryPutItemWithAutoEquipCheck(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
|
||||
{
|
||||
// Does not auto-equip the item if specified and no suitable any slot found (for example handcuffs are not auto-equipped)
|
||||
if (item.AllowedSlots.Contains(InvSlotType.Any))
|
||||
@@ -148,7 +162,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// If there is room, puts the item in the inventory and returns true, otherwise returns false
|
||||
/// </summary>
|
||||
public override bool TryPutItem(Item item, Character user, List<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
|
||||
public override bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
|
||||
{
|
||||
if (allowedSlots == null || !allowedSlots.Any()) { return false; }
|
||||
if (item == null)
|
||||
@@ -174,7 +188,7 @@ namespace Barotrauma
|
||||
int currentSlot = -1;
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (Items[i] == item)
|
||||
if (slots[i].Contains(item))
|
||||
{
|
||||
currentSlot = i;
|
||||
if (allowedSlots.Any(a => a.HasFlag(SlotTypes[i])))
|
||||
@@ -208,18 +222,18 @@ namespace Barotrauma
|
||||
bool free = true;
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && Items[i] != null && Items[i] != item)
|
||||
if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Items.Any(it => it != item))
|
||||
{
|
||||
#if CLIENT
|
||||
if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; }
|
||||
#endif
|
||||
if (!Items[i].AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(Items[i], character, new List<InvSlotType> { InvSlotType.Any }, true))
|
||||
if (!slots[i].First().AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(slots[i].FirstOrDefault(), character, new List<InvSlotType> { InvSlotType.Any }, true))
|
||||
{
|
||||
free = false;
|
||||
#if CLIENT
|
||||
for (int j = 0; j < capacity; j++)
|
||||
{
|
||||
if (slots != null && Items[j] == Items[i]) slots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f);
|
||||
if (visualSlots != null && slots[j] == slots[i]) { visualSlots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -230,7 +244,7 @@ namespace Barotrauma
|
||||
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && Items[i] == null)
|
||||
if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Empty())
|
||||
{
|
||||
#if CLIENT
|
||||
if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; }
|
||||
@@ -238,7 +252,7 @@ namespace Barotrauma
|
||||
bool removeFromOtherSlots = item.ParentInventory != this;
|
||||
if (placedInSlot == -1 && inWrongSlot)
|
||||
{
|
||||
if (!hideEmptySlot[i] || SlotTypes[currentSlot] != InvSlotType.Any) removeFromOtherSlots = true;
|
||||
if (!slots[i].HideIfEmpty || SlotTypes[currentSlot] != InvSlotType.Any) { removeFromOtherSlots = true; }
|
||||
}
|
||||
|
||||
PutItem(item, i, user, removeFromOtherSlots, createNetworkEvent);
|
||||
@@ -254,35 +268,51 @@ namespace Barotrauma
|
||||
|
||||
public int CheckIfAnySlotAvailable(Item item, bool inWrongSlot)
|
||||
{
|
||||
for (int i = 0; i < capacity; i++)
|
||||
//attempt to stack first
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (SlotTypes[i] != InvSlotType.Any) { continue; }
|
||||
if (!slots[i].Empty() && CanBePut(item, i))
|
||||
{
|
||||
if (SlotTypes[i] != InvSlotType.Any) continue;
|
||||
if (Items[i] == item)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (SlotTypes[i] != InvSlotType.Any) continue;
|
||||
if (inWrongSlot)
|
||||
{
|
||||
if (Items[i] != item && Items[i] != null) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Items[i] != null) continue;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
}
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (SlotTypes[i] != InvSlotType.Any) { continue; }
|
||||
if (slots[i].Contains(item))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (SlotTypes[i] != InvSlotType.Any) { continue; }
|
||||
if (CanBePut(item, i))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (SlotTypes[i] != InvSlotType.Any) { continue; }
|
||||
if (inWrongSlot)
|
||||
{
|
||||
//another item already in the slot
|
||||
if (slots[i].Any() && slots[i].Items.Any(it => it != item)) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!CanBePut(item, i)) { continue; }
|
||||
}
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true)
|
||||
{
|
||||
if (index < 0 || index >= Items.Length)
|
||||
if (index < 0 || index >= slots.Length)
|
||||
{
|
||||
string errorMsg = "CharacterInventory.TryPutItem failed: index was out of range(" + index + ").\n" + Environment.StackTrace.CleanupStackTrace();
|
||||
GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
@@ -292,18 +322,16 @@ namespace Barotrauma
|
||||
if (PersonalSlots.HasFlag(SlotTypes[index])) { hidePersonalSlots = false; }
|
||||
#endif
|
||||
//there's already an item in the slot
|
||||
if (Items[index] != null)
|
||||
if (slots[index].Any())
|
||||
{
|
||||
if (Items[index] == item) return false;
|
||||
|
||||
if (slots[index].Contains(item)) { return false; }
|
||||
return base.TryPutItem(item, index, allowSwapping, allowCombine, user, createNetworkEvent);
|
||||
}
|
||||
|
||||
if (SlotTypes[index] == InvSlotType.Any)
|
||||
{
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any)) return false;
|
||||
if (Items[index] != null) return Items[index] == item;
|
||||
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any)) { return false; }
|
||||
if (slots[index].Any()) { return slots[index].Contains(item); }
|
||||
PutItem(item, index, user, true, createNetworkEvent);
|
||||
return true;
|
||||
}
|
||||
@@ -311,28 +339,24 @@ namespace Barotrauma
|
||||
InvSlotType placeToSlots = InvSlotType.None;
|
||||
|
||||
bool slotsFree = true;
|
||||
List<InvSlotType> allowedSlots = item.AllowedSlots;
|
||||
foreach (InvSlotType allowedSlot in allowedSlots)
|
||||
foreach (InvSlotType allowedSlot in item.AllowedSlots)
|
||||
{
|
||||
if (!allowedSlot.HasFlag(SlotTypes[index])) continue;
|
||||
if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; }
|
||||
#if CLIENT
|
||||
if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; }
|
||||
#endif
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (allowedSlot.HasFlag(SlotTypes[i]) && Items[i] != null && Items[i] != item)
|
||||
if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item))
|
||||
{
|
||||
slotsFree = false;
|
||||
break;
|
||||
}
|
||||
|
||||
placeToSlots = allowedSlot;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!slotsFree) return false;
|
||||
if (!slotsFree) { return false; }
|
||||
|
||||
return TryPutItem(item, user, new List<InvSlotType>() { placeToSlots }, createNetworkEvent);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,16 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable
|
||||
{
|
||||
private static List<DockingPort> list = new List<DockingPort>();
|
||||
public enum DirectionType
|
||||
{
|
||||
None,
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
private static readonly List<DockingPort> list = new List<DockingPort>();
|
||||
public static IEnumerable<DockingPort> List
|
||||
{
|
||||
get { return list; }
|
||||
@@ -37,7 +46,7 @@ namespace Barotrauma.Items.Components
|
||||
//force the sub to the correct position
|
||||
const float ForceLockDelay = 1.0f;
|
||||
|
||||
public int DockingDir { get; private set; }
|
||||
public int DockingDir { get; set; }
|
||||
|
||||
[Serialize("32.0,32.0", false, description: "How close the docking port has to be to another port to dock.")]
|
||||
public Vector2 DistanceTolerance { get; set; }
|
||||
@@ -63,6 +72,10 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(DirectionType.None, false, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n"+
|
||||
"Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")]
|
||||
public DirectionType ForceDockingDirection { get; set; }
|
||||
|
||||
public DockingPort DockingTarget { get; private set; }
|
||||
|
||||
public Door Door { get; private set; }
|
||||
@@ -89,6 +102,11 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLocked
|
||||
{
|
||||
get { return joint is WeldJoint || DockingTarget?.joint is WeldJoint; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Automatically cleared after docking -> no need to unregister
|
||||
/// </summary>
|
||||
@@ -311,10 +329,10 @@ namespace Barotrauma.Items.Components
|
||||
joint = null;
|
||||
}
|
||||
|
||||
Vector2 offset = (IsHorizontal ?
|
||||
Vector2 offset = IsHorizontal ?
|
||||
Vector2.UnitX * DockingDir :
|
||||
Vector2.UnitY * DockingDir);
|
||||
offset *= DockedDistance * 0.5f;
|
||||
Vector2.UnitY * DockingDir;
|
||||
offset *= DockedDistance * 0.5f * item.Scale;
|
||||
|
||||
Vector2 pos1 = item.WorldPosition + offset;
|
||||
|
||||
@@ -346,6 +364,14 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public int GetDir(DockingPort dockingTarget = null)
|
||||
{
|
||||
int forcedDockingDir = GetForcedDockingDir();
|
||||
if (forcedDockingDir != 0) { return forcedDockingDir; }
|
||||
if (dockingTarget != null)
|
||||
{
|
||||
forcedDockingDir = -dockingTarget.GetForcedDockingDir();
|
||||
if (forcedDockingDir != 0) { return forcedDockingDir; }
|
||||
}
|
||||
|
||||
if (DockingDir != 0) { return DockingDir; }
|
||||
|
||||
if (Door != null && Door.LinkedGap.linkedTo.Count > 0)
|
||||
@@ -390,9 +416,10 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
if (dockingTarget != null)
|
||||
{
|
||||
return IsHorizontal ?
|
||||
int dir = IsHorizontal ?
|
||||
Math.Sign(dockingTarget.item.WorldPosition.X - item.WorldPosition.X) :
|
||||
Math.Sign(dockingTarget.item.WorldPosition.Y - item.WorldPosition.Y);
|
||||
if (dir != 0) { return dir; }
|
||||
}
|
||||
if (item.Submarine != null)
|
||||
{
|
||||
@@ -404,6 +431,22 @@ namespace Barotrauma.Items.Components
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int GetForcedDockingDir()
|
||||
{
|
||||
switch (ForceDockingDirection)
|
||||
{
|
||||
case DirectionType.Left:
|
||||
return -1;
|
||||
case DirectionType.Right:
|
||||
return 1;
|
||||
case DirectionType.Top:
|
||||
return 1;
|
||||
case DirectionType.Bottom:
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void ConnectWireBetweenPorts()
|
||||
{
|
||||
Wire wire = item.GetComponent<Wire>();
|
||||
@@ -491,8 +534,9 @@ namespace Barotrauma.Items.Components
|
||||
subs = new Submarine[] { DockingTarget.item.Submarine, item.Submarine };
|
||||
}
|
||||
|
||||
hullRects[0] = new Rectangle(hullRects[0].Center.X, hullRects[0].Y, ((int)DockedDistance / 2), hullRects[0].Height);
|
||||
hullRects[1] = new Rectangle(hullRects[1].Center.X - ((int)DockedDistance / 2), hullRects[1].Y, ((int)DockedDistance / 2), hullRects[1].Height);
|
||||
int scaledDockedDistance = (int)(DockedDistance / 2 * item.Scale);
|
||||
hullRects[0] = new Rectangle(hullRects[0].Center.X, hullRects[0].Y, scaledDockedDistance, hullRects[0].Height);
|
||||
hullRects[1] = new Rectangle(hullRects[1].Center.X - scaledDockedDistance, hullRects[1].Y, scaledDockedDistance, hullRects[1].Height);
|
||||
|
||||
//expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls
|
||||
int leftSubRightSide = int.MinValue, rightSubLeftSide = int.MaxValue;
|
||||
@@ -588,8 +632,9 @@ namespace Barotrauma.Items.Components
|
||||
subs = new Submarine[] { DockingTarget.item.Submarine, item.Submarine };
|
||||
}
|
||||
|
||||
hullRects[0] = new Rectangle(hullRects[0].X, hullRects[0].Y + (int)(-hullRects[0].Height + DockedDistance) / 2, hullRects[0].Width, ((int)DockedDistance / 2));
|
||||
hullRects[1] = new Rectangle(hullRects[1].X, hullRects[1].Y - hullRects[1].Height / 2, hullRects[1].Width, ((int)DockedDistance / 2));
|
||||
int scaledDockedDistance = (int)(DockedDistance / 2 * item.Scale);
|
||||
hullRects[0] = new Rectangle(hullRects[0].X, hullRects[0].Y - hullRects[0].Height / 2 + scaledDockedDistance, hullRects[0].Width, scaledDockedDistance);
|
||||
hullRects[1] = new Rectangle(hullRects[1].X, hullRects[1].Y - hullRects[1].Height / 2, hullRects[1].Width, scaledDockedDistance);
|
||||
|
||||
//expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls
|
||||
int upperSubBottom = int.MaxValue, lowerSubTop = int.MinValue;
|
||||
@@ -908,7 +953,6 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (joint is DistanceJoint)
|
||||
{
|
||||
item.SendSignal(0, "0", "state_out", null);
|
||||
dockingState = MathHelper.Lerp(dockingState, 0.5f, deltaTime * 10.0f);
|
||||
|
||||
forceLockTimer += deltaTime;
|
||||
@@ -918,9 +962,7 @@ namespace Barotrauma.Items.Components
|
||||
if (jointDiff.LengthSquared() > 0.04f * 0.04f && forceLockTimer < ForceLockDelay)
|
||||
{
|
||||
float totalMass = item.Submarine.PhysicsBody.Mass + DockingTarget.item.Submarine.PhysicsBody.Mass;
|
||||
float massRatio1 = 1.0f;
|
||||
float massRatio2 = 1.0f;
|
||||
|
||||
float massRatio1, massRatio2;
|
||||
if (item.Submarine.PhysicsBody.BodyType != BodyType.Dynamic)
|
||||
{
|
||||
massRatio1 = 0.0f;
|
||||
@@ -954,11 +996,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
doorBody.Enabled = DockingTarget.Door.Body.Enabled;
|
||||
}
|
||||
|
||||
item.SendSignal(0, "1", "state_out", null);
|
||||
|
||||
dockingState = MathHelper.Lerp(dockingState, 1.0f, deltaTime * 10.0f);
|
||||
}
|
||||
|
||||
item.SendSignal(0, IsLocked ? "1" : "0", "state_out", null);
|
||||
}
|
||||
if (!obstructedWayPointsDisabled && dockingState >= 0.99f)
|
||||
{
|
||||
@@ -985,7 +1026,8 @@ namespace Barotrauma.Items.Components
|
||||
if (initialized) { return; }
|
||||
initialized = true;
|
||||
|
||||
float closestDist = 30.0f * 30.0f;
|
||||
float maxXDist = (item.Prefab.sprite.size.X * item.Prefab.Scale) / 2;
|
||||
float closestYDist = (item.Prefab.sprite.size.Y * item.Prefab.Scale) / 2;
|
||||
foreach (Item it in Item.ItemList)
|
||||
{
|
||||
if (it.Submarine != item.Submarine) { continue; }
|
||||
@@ -993,11 +1035,22 @@ namespace Barotrauma.Items.Components
|
||||
var doorComponent = it.GetComponent<Door>();
|
||||
if (doorComponent == null || doorComponent.IsHorizontal == IsHorizontal) { continue; }
|
||||
|
||||
float distSqr = Vector2.DistanceSquared(item.Position, it.Position);
|
||||
if (distSqr < closestDist)
|
||||
float yDist = Math.Abs(it.Position.Y - item.Position.Y);
|
||||
if (item.linkedTo.Contains(it))
|
||||
{
|
||||
// If there's a door linked to the docking port, always treat it close enough.
|
||||
yDist = Math.Min(closestYDist, yDist);
|
||||
}
|
||||
else if (Math.Abs(it.Position.X - item.Position.X) > maxXDist)
|
||||
{
|
||||
// Too far left/right
|
||||
continue;
|
||||
}
|
||||
|
||||
if (yDist <= closestYDist)
|
||||
{
|
||||
Door = doorComponent;
|
||||
closestDist = distSqr;
|
||||
closestYDist = yDist;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ namespace Barotrauma.Items.Components
|
||||
private Rectangle doorRect;
|
||||
|
||||
private bool isBroken;
|
||||
|
||||
public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck;
|
||||
|
||||
public bool IsBroken
|
||||
{
|
||||
@@ -283,12 +285,6 @@ namespace Barotrauma.Items.Components
|
||||
return isBroken || base.HasRequiredItems(character, addMessage, msg);
|
||||
}
|
||||
|
||||
public bool CanBeOpenedWithoutTools(Character character)
|
||||
{
|
||||
if (isBroken) { return true; }
|
||||
return HasAccess(character);
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
if (item.Condition < RepairThreshold) { return true; }
|
||||
@@ -642,6 +638,19 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void OnFailedToOpen();
|
||||
|
||||
public override bool HasAccess(Character character)
|
||||
{
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (HasIntegratedButtons)
|
||||
{
|
||||
return base.HasAccess(character);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character));
|
||||
}
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
|
||||
{
|
||||
if (IsStuck || IsJammed) { return; }
|
||||
|
||||
@@ -395,6 +395,9 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize("1,1,1,1", true, "Probability for the plant to grow in a direction.")]
|
||||
public Vector4 GrowthWeights { get; set; }
|
||||
|
||||
[Serialize(0.0f, true, "How much damage is taken from fires.")]
|
||||
public float FireVulnerability { get; set; }
|
||||
|
||||
private const float increasedDeathSpeed = 10f;
|
||||
private bool accelerateDeath;
|
||||
private float health;
|
||||
@@ -418,6 +421,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private int productDelay;
|
||||
private int vineDelay;
|
||||
private float fireCheckCooldown;
|
||||
|
||||
public readonly List<ProducedItem> ProducedItems = new List<ProducedItem>();
|
||||
public readonly List<VineTile> Vines = new List<VineTile>();
|
||||
@@ -489,11 +493,15 @@ namespace Barotrauma.Items.Components
|
||||
if (Health > 0)
|
||||
{
|
||||
GrowVines(planter, slot);
|
||||
Health -= accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness;
|
||||
|
||||
// fertilizer makes the plant tick faster, compensate by halving water requirement
|
||||
float multipler = planter.Fertilizer > 0 ? 0.5f : 1f;
|
||||
|
||||
Health -= (accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness) * multipler;
|
||||
|
||||
if (planter.Item.InWater)
|
||||
{
|
||||
Health -= FloodTolerance;
|
||||
Health -= FloodTolerance * multipler;
|
||||
}
|
||||
#if SERVER
|
||||
if (FullyGrown)
|
||||
@@ -630,6 +638,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
base.Update(deltaTime, cam);
|
||||
|
||||
UpdateFires(deltaTime);
|
||||
|
||||
#if CLIENT
|
||||
foreach (VineTile vine in Vines)
|
||||
{
|
||||
@@ -640,6 +650,29 @@ namespace Barotrauma.Items.Components
|
||||
CheckPlantState();
|
||||
}
|
||||
|
||||
private void UpdateFires(float deltaTime)
|
||||
{
|
||||
if (!Decayed && item.CurrentHull?.FireSources is { } fireSources && FireVulnerability > 0f)
|
||||
{
|
||||
if (fireCheckCooldown <= 0)
|
||||
{
|
||||
foreach (FireSource source in fireSources)
|
||||
{
|
||||
if (source.IsInDamageRange(item.WorldPosition, source.DamageRange))
|
||||
{
|
||||
Health -= FireVulnerability;
|
||||
}
|
||||
}
|
||||
|
||||
fireCheckCooldown = 5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
fireCheckCooldown -= deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void GrowVines(Planter planter, PlantSlot slot)
|
||||
{
|
||||
if (FullyGrown) { return; }
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components
|
||||
protected Vector2[] handlePos;
|
||||
private readonly Vector2[] scaledHandlePos;
|
||||
|
||||
private InputType prevPickKey;
|
||||
private readonly InputType prevPickKey;
|
||||
private string prevMsg;
|
||||
private Dictionary<RelatedItem.RelationType, List<RelatedItem>> prevRequiredItems;
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private float swingState;
|
||||
|
||||
private Character prevEquipper;
|
||||
|
||||
private bool attachable, attached, attachedByDefault;
|
||||
private Voronoi2.VoronoiCell attachTargetCell;
|
||||
private readonly PhysicsBody body;
|
||||
@@ -301,11 +303,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
item.SetTransform(picker.SimPosition, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
picker.DeselectItem(item);
|
||||
picker.Inventory.RemoveItem(item);
|
||||
picker = null;
|
||||
}
|
||||
@@ -340,34 +340,33 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
bool alreadyEquipped = character.HasEquippedItem(item);
|
||||
bool canSelect = picker.TrySelectItem(item);
|
||||
|
||||
if (canSelect || picker.HasEquippedItem(item))
|
||||
if (picker.HasEquippedItem(item))
|
||||
{
|
||||
if (!canSelect)
|
||||
{
|
||||
character.DeselectItem(item);
|
||||
}
|
||||
|
||||
item.body.Enabled = true;
|
||||
item.body.PhysEnabled = false;
|
||||
IsActive = true;
|
||||
|
||||
#if SERVER
|
||||
if (!alreadyEquipped) GameServer.Log(GameServer.CharacterLogName(character) + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction);
|
||||
if (picker != prevEquipper) { GameServer.Log(GameServer.CharacterLogName(character) + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction); }
|
||||
#endif
|
||||
prevEquipper = picker;
|
||||
}
|
||||
else
|
||||
{
|
||||
prevEquipper = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unequip(Character character)
|
||||
{
|
||||
if (picker == null) { return; }
|
||||
|
||||
picker.DeselectItem(item);
|
||||
#if SERVER
|
||||
GameServer.Log(GameServer.CharacterLogName(character) + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction);
|
||||
if (prevEquipper != null)
|
||||
{
|
||||
GameServer.Log(GameServer.CharacterLogName(character) + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction);
|
||||
}
|
||||
#endif
|
||||
|
||||
prevEquipper = null;
|
||||
if (picker == null) { return; }
|
||||
item.body.PhysEnabled = true;
|
||||
item.body.Enabled = false;
|
||||
IsActive = false;
|
||||
@@ -488,13 +487,12 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
var containedItems = item.OwnInventory?.Items;
|
||||
var containedItems = item.OwnInventory?.AllItems;
|
||||
if (containedItems != null)
|
||||
{
|
||||
foreach (Item contained in containedItems)
|
||||
{
|
||||
if (contained == null) { continue; }
|
||||
if (contained.body == null) { continue; }
|
||||
if (contained?.body == null) { continue; }
|
||||
contained.SetTransform(item.SimPosition, contained.body.Rotation);
|
||||
}
|
||||
}
|
||||
@@ -673,7 +671,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
item.Submarine = picker.Submarine;
|
||||
|
||||
if (picker.HasSelectedItem(item))
|
||||
if (picker.HeldItems.Contains(item))
|
||||
{
|
||||
scaledHandlePos[0] = handlePos[0] * item.Scale;
|
||||
scaledHandlePos[1] = handlePos[1] * item.Scale;
|
||||
|
||||
@@ -42,13 +42,13 @@ namespace Barotrauma.Items.Components
|
||||
public override void Equip(Character character)
|
||||
{
|
||||
base.Equip(character);
|
||||
character.Info.CheckDisguiseStatus(true, this);
|
||||
character.Info?.CheckDisguiseStatus(true, this);
|
||||
}
|
||||
|
||||
public override void Unequip(Character character)
|
||||
{
|
||||
base.Unequip(character);
|
||||
character.Info.CheckDisguiseStatus(true, this);
|
||||
character.Info?.CheckDisguiseStatus(true, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,13 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(1.0f, false, description: "How much the position of the item can vary from the wall the item spawns on.")]
|
||||
public float RandomOffsetFromWall
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool Attached
|
||||
{
|
||||
get { return holdable != null && holdable.Attached; }
|
||||
|
||||
@@ -50,6 +50,11 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines items that boost the weapon functionality, like battery cell for stun batons.
|
||||
/// </summary>
|
||||
public readonly string[] PreferredContainedItems;
|
||||
|
||||
public MeleeWeapon(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -61,6 +66,7 @@ namespace Barotrauma.Items.Components
|
||||
item.IsShootable = true;
|
||||
// TODO: should define this in xml if we have melee weapons that don't require aim to use
|
||||
item.RequireAimToUse = true;
|
||||
PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true);
|
||||
}
|
||||
|
||||
public override void Equip(Character character)
|
||||
@@ -76,11 +82,9 @@ namespace Barotrauma.Items.Components
|
||||
if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) { return false; }
|
||||
|
||||
//don't allow hitting if the character is already hitting with another weapon
|
||||
for (int i = 0; i < 2; i++ )
|
||||
foreach (Item heldItem in character.HeldItems)
|
||||
{
|
||||
if (character.SelectedItems[i] == null || character.SelectedItems[i] == Item) { continue; }
|
||||
|
||||
var otherWeapon = character.SelectedItems[i].GetComponent<MeleeWeapon>();
|
||||
var otherWeapon = heldItem.GetComponent<MeleeWeapon>();
|
||||
if (otherWeapon == null) { continue; }
|
||||
if (otherWeapon.hitting) { return false; }
|
||||
}
|
||||
@@ -143,7 +147,7 @@ namespace Barotrauma.Items.Components
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (!item.body.Enabled) { impactQueue.Clear(); return; }
|
||||
if (picker == null && !picker.HasSelectedItem(item)) { impactQueue.Clear(); IsActive = false; }
|
||||
if (picker == null && !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; }
|
||||
|
||||
while (impactQueue.Count > 0)
|
||||
{
|
||||
@@ -244,12 +248,14 @@ namespace Barotrauma.Items.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
//ignore collision if there's a wall between the user and the weapon to prevent hitting through walls
|
||||
contact.GetWorldManifold(out Vector2 normal, out var points);
|
||||
|
||||
//ignore collision if there's a wall between the user and the contact point to prevent hitting through walls
|
||||
if (Submarine.PickBody(User.AnimController.AimSourceSimPos,
|
||||
item.SimPosition,
|
||||
points[0],
|
||||
collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
|
||||
allowInsideFixture: true,
|
||||
customPredicate: (Fixture fixture) => { return fixture.CollidesWith.HasFlag(Physics.CollisionItem); }) != null)
|
||||
customPredicate: (Fixture fixture) => { return fixture.CollidesWith.HasFlag(Physics.CollisionItem) && fixture.Body != f2.Body; }) != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
@@ -91,7 +92,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots))
|
||||
{
|
||||
if (!picker.HasSelectedItem(item) && item.body != null) item.body.Enabled = false;
|
||||
if (!picker.HeldItems.Contains(item) && item.body != null) { item.body.Enabled = false; }
|
||||
this.picker = picker;
|
||||
|
||||
for (int i = item.linkedTo.Count - 1; i >= 0; i--)
|
||||
|
||||
@@ -71,11 +71,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
character.AnimController.Collider.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
|
||||
if (character.SelectedItems[0] == item)
|
||||
if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand))
|
||||
{
|
||||
character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
}
|
||||
if (character.SelectedItems[1] == item)
|
||||
if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
|
||||
{
|
||||
character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public Projectile FindProjectile(bool triggerOnUseOnContainers = false)
|
||||
{
|
||||
var containedItems = item.OwnInventory?.Items;
|
||||
var containedItems = item.OwnInventory?.AllItemsMod;
|
||||
if (containedItems == null) { return null; }
|
||||
|
||||
foreach (Item item in containedItems)
|
||||
@@ -166,7 +166,7 @@ namespace Barotrauma.Items.Components
|
||||
foreach (Item it in containedItems)
|
||||
{
|
||||
if (it == null) { continue; }
|
||||
var containedSubItems = it.OwnInventory?.Items;
|
||||
var containedSubItems = it.OwnInventory?.AllItemsMod;
|
||||
if (containedSubItems == null) { continue; }
|
||||
foreach (Item subItem in containedSubItems)
|
||||
{
|
||||
|
||||
@@ -87,6 +87,13 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(false, false, description: "Can the item repair things through holes in walls.")]
|
||||
public bool RepairThroughHoles { get; set; }
|
||||
|
||||
|
||||
[Serialize(100.0f, false, description: "How far two walls need to not be considered overlapping and to stop the ray.")]
|
||||
public float MaxOverlappingWallDist
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Serialize(true, false, description: "Can the item hit broken doors.")]
|
||||
public bool HitItems { get; set; }
|
||||
|
||||
@@ -320,9 +327,14 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
var bodies = Submarine.PickBodies(rayStart, rayEnd, ignoredBodies, collisionCategories,
|
||||
ignoreSensors: false,
|
||||
customPredicate: (Fixture f) =>
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure || (f.Body?.UserData is Item it && it.GetComponent<Planter>() != null)) { return false; }
|
||||
if (f.IsSensor)
|
||||
{
|
||||
if (RepairThroughHoles && f.Body?.UserData is Structure) { return false; }
|
||||
if (f.Body?.UserData is PhysicsBody) { return false; }
|
||||
}
|
||||
if (f.Body?.UserData is Item it && it.GetComponent<Planter>() != null) { return false; }
|
||||
if (f.Body?.UserData as string == "ruinroom") { return false; }
|
||||
if (f.Body?.UserData is VineTile && !(FireDamage > 0)) { return false; }
|
||||
return true;
|
||||
@@ -361,12 +373,13 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
//if repairing through walls is not allowed and the next wall is more than 100 pixels away from the previous one, stop here
|
||||
//(= repairing multiple overlapping walls is allowed as long as the edges of the walls are less than 100 pixels apart)
|
||||
//(= repairing multiple overlapping walls is allowed as long as the edges of the walls are less than MaxOverlappingWallDist pixels apart)
|
||||
float thisBodyFraction = Submarine.LastPickedBodyDist(body);
|
||||
if (!RepairThroughWalls && lastHitType == typeof(Structure) && Range * (thisBodyFraction - lastPickedFraction) > 100.0f)
|
||||
if (!RepairThroughWalls && lastHitType == typeof(Structure) && Range * (thisBodyFraction - lastPickedFraction) > MaxOverlappingWallDist)
|
||||
{
|
||||
break;
|
||||
}
|
||||
pickedPosition = rayStart + (rayEnd - rayStart) * thisBodyFraction;
|
||||
if (FixBody(user, deltaTime, degreeOfSuccess, body))
|
||||
{
|
||||
lastPickedFraction = thisBodyFraction;
|
||||
@@ -376,13 +389,16 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
FixBody(user, deltaTime, degreeOfSuccess,
|
||||
Submarine.PickBody(rayStart, rayEnd,
|
||||
ignoredBodies, collisionCategories,
|
||||
var pickedBody = Submarine.PickBody(rayStart, rayEnd,
|
||||
ignoredBodies, collisionCategories,
|
||||
ignoreSensors: false,
|
||||
customPredicate: (Fixture f) =>
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; }
|
||||
if (f.IsSensor)
|
||||
{
|
||||
if (RepairThroughHoles && f.Body?.UserData is Structure) { return false; }
|
||||
if (f.Body?.UserData is PhysicsBody) { return false; }
|
||||
}
|
||||
if (f.Body?.UserData as string == "ruinroom") { return false; }
|
||||
if (f.Body?.UserData is VineTile && !(FireDamage > 0)) { return false; }
|
||||
|
||||
@@ -398,9 +414,11 @@ namespace Barotrauma.Items.Components
|
||||
if (targetItem.Condition <= 0) { return false; }
|
||||
}
|
||||
}
|
||||
return f.Body?.UserData != null;
|
||||
return f.Body?.UserData != null;
|
||||
},
|
||||
allowInsideFixture: true));
|
||||
allowInsideFixture: true);
|
||||
pickedPosition = Submarine.LastPickedPosition;
|
||||
FixBody(user, deltaTime, degreeOfSuccess, pickedBody);
|
||||
lastPickedFraction = Submarine.LastPickedFraction;
|
||||
}
|
||||
|
||||
@@ -495,8 +513,6 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (targetBody?.UserData == null) { return false; }
|
||||
|
||||
pickedPosition = Submarine.LastPickedPosition;
|
||||
|
||||
if (targetBody.UserData is Structure targetStructure)
|
||||
{
|
||||
if (targetStructure.IsPlatform) { return false; }
|
||||
@@ -526,8 +542,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)
|
||||
{
|
||||
var levelWall = Level.Loaded?.ExtraWalls.Find(w => w.Body == cell.Body) as DestructibleLevelWall;
|
||||
if (levelWall != null)
|
||||
if (Level.Loaded?.ExtraWalls.Find(w => w.Body == cell.Body) is DestructibleLevelWall levelWall)
|
||||
{
|
||||
levelWall.AddDamage(-LevelWallFixAmount * deltaTime, item.WorldPosition);
|
||||
}
|
||||
@@ -579,7 +594,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (targetBody.UserData is Item targetItem)
|
||||
{
|
||||
if (!HitItems || targetItem.NonInteractable) { return false; }
|
||||
if (!HitItems || !targetItem.IsInteractable(user)) { return false; }
|
||||
|
||||
var levelResource = targetItem.GetComponent<LevelResource>();
|
||||
if (levelResource != null && levelResource.Attached &&
|
||||
@@ -594,7 +609,7 @@ namespace Barotrauma.Items.Components
|
||||
levelResource.DeattachTimer / levelResource.DeattachDuration,
|
||||
GUI.Style.Red, GUI.Style.Green, "progressbar.deattaching");
|
||||
#endif
|
||||
FixItemProjSpecific(user, deltaTime, targetItem);
|
||||
FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -620,7 +635,7 @@ namespace Barotrauma.Items.Components
|
||||
targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f);
|
||||
}
|
||||
|
||||
FixItemProjSpecific(user, deltaTime, targetItem);
|
||||
FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: true);
|
||||
return true;
|
||||
}
|
||||
else if (targetBody.UserData is BallastFloraBranch branch)
|
||||
@@ -635,7 +650,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void FixStructureProjSpecific(Character user, float deltaTime, Structure targetStructure, int sectionIndex);
|
||||
partial void FixCharacterProjSpecific(Character user, float deltaTime, Character targetCharacter);
|
||||
partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem);
|
||||
partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem, bool showProgressBar);
|
||||
|
||||
private float sinTime;
|
||||
private float repairTimer;
|
||||
@@ -643,74 +658,71 @@ namespace Barotrauma.Items.Components
|
||||
private readonly float repairTimeOut = 5;
|
||||
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
{
|
||||
if (!(objective.OperateTarget is Gap leak)) { return true; }
|
||||
if (leak.Submarine == null) { return true; }
|
||||
if (!(objective.OperateTarget is Gap leak))
|
||||
{
|
||||
Reset();
|
||||
return true;
|
||||
}
|
||||
if (leak.Submarine == null)
|
||||
{
|
||||
Reset();
|
||||
return true;
|
||||
}
|
||||
if (leak != previousGap)
|
||||
{
|
||||
sinTime = 0;
|
||||
repairTimer = 0;
|
||||
Reset();
|
||||
previousGap = leak;
|
||||
}
|
||||
Vector2 fromCharacterToLeak = leak.WorldPosition - character.WorldPosition;
|
||||
float dist = fromCharacterToLeak.Length();
|
||||
float reach = AIObjectiveFixLeak.CalculateReach(this, character);
|
||||
|
||||
//too far away -> consider this done and hope the AI is smart enough to move closer
|
||||
if (dist > reach * 2) { return true; }
|
||||
character.AIController.SteeringManager.Reset();
|
||||
//steer closer if almost in range
|
||||
if (dist > reach)
|
||||
if (dist > reach * 3)
|
||||
{
|
||||
if (character.AnimController.InWater)
|
||||
// Too far away -> consider this done and hope the AI is smart enough to move closer
|
||||
Reset();
|
||||
return true;
|
||||
}
|
||||
character.AIController.SteeringManager.Reset();
|
||||
if (!character.AnimController.InWater)
|
||||
{
|
||||
// TODO: use the collider size?
|
||||
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
|
||||
Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f)
|
||||
{
|
||||
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
|
||||
((HumanoidAnimController)character.AnimController).Crouching = true;
|
||||
}
|
||||
}
|
||||
if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.inWater))
|
||||
{
|
||||
// Steer closer
|
||||
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
|
||||
{
|
||||
// Swimming inside the sub
|
||||
if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && (indoorSteering.CurrentPath.Unreachable || indoorSteering.CurrentPath.Finished))
|
||||
{
|
||||
// Swimming inside the sub
|
||||
if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && indoorSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
Vector2 dir = Vector2.Normalize(fromCharacterToLeak);
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, dir);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
|
||||
}
|
||||
Vector2 dir = Vector2.Normalize(fromCharacterToLeak);
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, dir);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Swimming outside the sub
|
||||
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: use the collider size?
|
||||
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
|
||||
Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f)
|
||||
{
|
||||
((HumanoidAnimController)character.AnimController).Crouching = true;
|
||||
}
|
||||
Vector2 standPos = new Vector2(Math.Sign(-fromCharacterToLeak.X), Math.Sign(-fromCharacterToLeak.Y)) / 2;
|
||||
if (leak.IsHorizontal)
|
||||
{
|
||||
standPos.X *= 2;
|
||||
standPos.Y = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
standPos.X = 0;
|
||||
}
|
||||
character.AIController.SteeringManager.SteeringSeek(standPos);
|
||||
// Swimming outside the sub
|
||||
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
|
||||
}
|
||||
}
|
||||
if (dist < reach / 2)
|
||||
else if (dist < reach * 0.25f)
|
||||
{
|
||||
// Too close -> steer away
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
|
||||
}
|
||||
else if (dist < reach * 2)
|
||||
if (dist <= reach)
|
||||
{
|
||||
// In or almost in range
|
||||
// In range
|
||||
character.CursorPosition = leak.WorldPosition;
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
@@ -734,46 +746,52 @@ namespace Barotrauma.Items.Components
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.RequireAimToUse)
|
||||
{
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
sinTime += deltaTime * 5;
|
||||
}
|
||||
// Press the trigger only when the tool is approximately facing the target.
|
||||
Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
|
||||
var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak);
|
||||
if (angle < MathHelper.PiOver4)
|
||||
{
|
||||
if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i)
|
||||
if (item.RequireAimToUse)
|
||||
{
|
||||
var door = i.GetComponent<Door>();
|
||||
// Hit a door, abandon so that we don't weld it shut.
|
||||
return door != null && !door.IsOpen && !door.IsBroken;
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
sinTime += deltaTime * 5;
|
||||
}
|
||||
// Check that we don't hit any friendlies
|
||||
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
|
||||
// Press the trigger only when the tool is approximately facing the target.
|
||||
Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
|
||||
var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak);
|
||||
if (angle < MathHelper.PiOver4)
|
||||
{
|
||||
if (hit.UserData is Character c)
|
||||
if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i)
|
||||
{
|
||||
if (c == character) { return false; }
|
||||
return HumanAIController.IsFriendly(character, c);
|
||||
var door = i.GetComponent<Door>();
|
||||
// Hit a door, abandon so that we don't weld it shut.
|
||||
return door != null && !door.CanBeTraversed;
|
||||
}
|
||||
return false;
|
||||
}))
|
||||
{
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
Use(deltaTime, character);
|
||||
repairTimer += deltaTime;
|
||||
if (repairTimer > repairTimeOut)
|
||||
// Check that we don't hit any friendlies
|
||||
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
|
||||
{
|
||||
if (hit.UserData is Character c)
|
||||
{
|
||||
if (c == character) { return false; }
|
||||
return HumanAIController.IsFriendly(character, c);
|
||||
}
|
||||
return false;
|
||||
}))
|
||||
{
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
Use(deltaTime, character);
|
||||
repairTimer += deltaTime;
|
||||
if (repairTimer > repairTimeOut)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow);
|
||||
DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow);
|
||||
#endif
|
||||
return true;
|
||||
Reset();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reset the timer so that we don't time out if the water forces push us away
|
||||
repairTimer = 0;
|
||||
}
|
||||
|
||||
bool leakFixed = (leak.Open <= 0.0f || leak.Removed) &&
|
||||
(leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1);
|
||||
@@ -791,6 +809,12 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
return leakFixed;
|
||||
|
||||
void Reset()
|
||||
{
|
||||
sinTime = 0;
|
||||
repairTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, IEnumerable<ISerializableEntity> targets)
|
||||
@@ -821,7 +845,7 @@ namespace Barotrauma.Items.Components
|
||||
foreach (ISerializableEntity target in targets)
|
||||
{
|
||||
if (!(target is Door door)) { continue; }
|
||||
if (!door.CanBeWelded || door.Item.NonInteractable) { continue; }
|
||||
if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; }
|
||||
for (int i = 0; i < effect.propertyNames.Length; i++)
|
||||
{
|
||||
string propertyName = effect.propertyNames[i];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
@@ -83,7 +84,7 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
if (picker == null || picker.Removed || !picker.HasSelectedItem(item))
|
||||
if (picker == null || picker.Removed || !picker.HeldItems.Contains(item))
|
||||
{
|
||||
IsActive = false;
|
||||
return;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user