v0.10.5.1
This commit is contained in:
@@ -90,6 +90,19 @@ namespace Barotrauma
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
|
||||
//switched control to some other character during the transition -> remove control again
|
||||
if (Character.Controlled != null)
|
||||
{
|
||||
prevControlled = Character.Controlled;
|
||||
if (RemoveControlFromCharacter)
|
||||
{
|
||||
#if CLIENT
|
||||
GameMain.LightManager.LosEnabled = false;
|
||||
#endif
|
||||
Character.Controlled = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevControlled != null && prevControlled.Removed)
|
||||
{
|
||||
prevControlled = null;
|
||||
|
||||
@@ -13,12 +13,6 @@ namespace Barotrauma
|
||||
|
||||
private AIState state;
|
||||
|
||||
protected void ResetAITarget()
|
||||
{
|
||||
_lastAiTarget = null;
|
||||
_selectedAiTarget = null;
|
||||
}
|
||||
|
||||
// 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).
|
||||
@@ -142,6 +136,17 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
ResetAITarget();
|
||||
}
|
||||
|
||||
protected void ResetAITarget()
|
||||
{
|
||||
_lastAiTarget = null;
|
||||
_selectedAiTarget = null;
|
||||
}
|
||||
|
||||
protected virtual void OnStateChanged(AIState from, AIState to) { }
|
||||
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
|
||||
|
||||
|
||||
@@ -153,6 +153,11 @@ namespace Barotrauma
|
||||
{
|
||||
throw new Exception($"Tried to create an enemy ai controller for human!");
|
||||
}
|
||||
if (Character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Pet
|
||||
Character.TeamID = Character.TeamType.FriendlyNPC;
|
||||
}
|
||||
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(c.SpeciesName);
|
||||
var mainElement = prefab.XDocument.Root.IsOverride() ? prefab.XDocument.Root.FirstElement() : prefab.XDocument.Root;
|
||||
targetMemories = new Dictionary<AITarget, AITargetMemory>();
|
||||
@@ -733,7 +738,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth))
|
||||
{
|
||||
LatchOntoAI?.DeattachFromBody();
|
||||
LatchOntoAI?.DeattachFromBody(cooldown: 2);
|
||||
Character.AnimController.ReleaseStuckLimbs();
|
||||
var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition);
|
||||
steeringManager.SteeringManual(deltaTime, velocity);
|
||||
@@ -1121,7 +1126,7 @@ namespace Barotrauma
|
||||
{
|
||||
IsSteeringThroughGap = true;
|
||||
wallTarget = null;
|
||||
LatchOntoAI?.DeattachFromBody();
|
||||
LatchOntoAI?.DeattachFromBody(cooldown: 2);
|
||||
Character.AnimController.ReleaseStuckLimbs();
|
||||
Hull targetHull = section.gap?.FlowTargetHull;
|
||||
float maxDistance = Math.Min(wall.Rect.Width, wall.Rect.Height);
|
||||
@@ -1303,7 +1308,7 @@ namespace Barotrauma
|
||||
|
||||
bool wasLatched = IsLatchedOnSub;
|
||||
Character.AnimController.ReleaseStuckLimbs();
|
||||
LatchOntoAI?.DeattachFromBody();
|
||||
LatchOntoAI?.DeattachFromBody(cooldown: 1);
|
||||
if (attacker == null || attacker.AiTarget == null) { return; }
|
||||
bool isFriendly = IsFriendly(Character, attacker);
|
||||
if (wasLatched)
|
||||
@@ -1544,39 +1549,6 @@ namespace Barotrauma
|
||||
//sight/hearing range
|
||||
public AITarget UpdateTargets(Character character, out CharacterParams.TargetParams targetingParams)
|
||||
{
|
||||
if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub)
|
||||
{
|
||||
var wall = SelectedAiTarget.Entity as Structure;
|
||||
if (wall == null)
|
||||
{
|
||||
wall = wallTarget?.Structure;
|
||||
}
|
||||
// The target is not a wall or it's not the same as we are attached to -> release
|
||||
bool releaseTarget = wall == null || !wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB);
|
||||
if (!releaseTarget)
|
||||
{
|
||||
for (int i = 0; i < wall.Sections.Length; i++)
|
||||
{
|
||||
if (CanPassThroughHole(wall, i))
|
||||
{
|
||||
releaseTarget = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (releaseTarget)
|
||||
{
|
||||
SelectedAiTarget = null;
|
||||
wallTarget = null;
|
||||
LatchOntoAI.DeattachFromBody(cooldown: 1);
|
||||
}
|
||||
else if (SelectedAiTarget?.Entity == wallTarget?.Structure)
|
||||
{
|
||||
// If attached to a valid target, just keep the target.
|
||||
// Priority not used in this case.
|
||||
targetingParams = null;
|
||||
return SelectedAiTarget;
|
||||
}
|
||||
}
|
||||
AITarget newTarget = null;
|
||||
targetValue = 0;
|
||||
selectedTargetMemory = null;
|
||||
@@ -1611,42 +1583,44 @@ namespace Barotrauma
|
||||
{
|
||||
targetingTag = tP.Tag;
|
||||
}
|
||||
else if (targetCharacter.AIController is EnemyAIController enemy)
|
||||
else
|
||||
{
|
||||
if (targetCharacter.Params.CompareGroup(Character.Params.Group))
|
||||
if (IsFriendly(Character, targetCharacter))
|
||||
{
|
||||
// Ignore targets that are in the same group (treat them like they were of the same species)
|
||||
continue;
|
||||
}
|
||||
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
|
||||
if (targetCharacter.AIController is EnemyAIController enemy)
|
||||
{
|
||||
targetingTag = "husk";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enemy.CombatStrength > CombatStrength)
|
||||
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
|
||||
{
|
||||
targetingTag = "stronger";
|
||||
targetingTag = "husk";
|
||||
}
|
||||
else if (enemy.CombatStrength < CombatStrength)
|
||||
else
|
||||
{
|
||||
targetingTag = "weaker";
|
||||
}
|
||||
if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee))
|
||||
{
|
||||
if (SelectedAiTarget == aiTarget)
|
||||
if (enemy.CombatStrength > CombatStrength)
|
||||
{
|
||||
// Freightened -> hold on to the target
|
||||
valueModifier *= 2;
|
||||
targetingTag = "stronger";
|
||||
}
|
||||
if (IsBeingChasedBy(targetCharacter))
|
||||
else if (enemy.CombatStrength < CombatStrength)
|
||||
{
|
||||
valueModifier *= 2;
|
||||
targetingTag = "weaker";
|
||||
}
|
||||
if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull))
|
||||
if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee))
|
||||
{
|
||||
// Inside but in a different room
|
||||
valueModifier /= 2;
|
||||
if (SelectedAiTarget == aiTarget)
|
||||
{
|
||||
// Freightened -> hold on to the target
|
||||
valueModifier *= 2;
|
||||
}
|
||||
if (IsBeingChasedBy(targetCharacter))
|
||||
{
|
||||
valueModifier *= 2;
|
||||
}
|
||||
if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull))
|
||||
{
|
||||
// Inside but in a different room
|
||||
valueModifier /= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1855,25 +1829,28 @@ namespace Barotrauma
|
||||
|
||||
if (valueModifier == 0.0f) { continue; }
|
||||
|
||||
if (SwarmBehavior != null && SwarmBehavior.Members.Any())
|
||||
if (targetingTag != "decoy")
|
||||
{
|
||||
// Halve the priority for each swarm mate targeting the same target -> reduces stacking
|
||||
foreach (Character otherCharacter in SwarmBehavior.Members)
|
||||
if (SwarmBehavior != null && SwarmBehavior.Members.Any())
|
||||
{
|
||||
if (otherCharacter == character) { continue; }
|
||||
if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
|
||||
valueModifier /= 2;
|
||||
// Halve the priority for each swarm mate targeting the same target -> reduces stacking
|
||||
foreach (Character otherCharacter in SwarmBehavior.Members)
|
||||
{
|
||||
if (otherCharacter == character) { continue; }
|
||||
if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
|
||||
valueModifier /= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The same as above, but using all the friendly characters in the level.
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
else
|
||||
{
|
||||
if (otherCharacter == character) { continue; }
|
||||
if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
|
||||
if (!IsFriendly(character, otherCharacter)) { continue; }
|
||||
valueModifier /= 2;
|
||||
// The same as above, but using all the friendly characters in the level.
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
if (otherCharacter == character) { continue; }
|
||||
if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
|
||||
if (!IsFriendly(character, otherCharacter)) { continue; }
|
||||
valueModifier /= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1969,7 +1946,34 @@ namespace Barotrauma
|
||||
SelectedAiTarget = newTarget;
|
||||
if (SelectedAiTarget != _previousAiTarget)
|
||||
{
|
||||
wallTarget = null;
|
||||
if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub)
|
||||
{
|
||||
if (!(SelectedAiTarget.Entity is Structure wall))
|
||||
{
|
||||
wall = wallTarget?.Structure;
|
||||
}
|
||||
// The target is not a wall or it's not the same as we are attached to -> release
|
||||
bool releaseTarget = wall == null || !wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB);
|
||||
if (!releaseTarget)
|
||||
{
|
||||
for (int i = 0; i < wall.Sections.Length; i++)
|
||||
{
|
||||
if (CanPassThroughHole(wall, i))
|
||||
{
|
||||
releaseTarget = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (releaseTarget)
|
||||
{
|
||||
wallTarget = null;
|
||||
LatchOntoAI.DeattachFromBody(cooldown: 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
wallTarget = null;
|
||||
}
|
||||
}
|
||||
return SelectedAiTarget;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Barotrauma
|
||||
{
|
||||
public static bool DisableCrewAI;
|
||||
|
||||
private AIObjectiveManager objectiveManager;
|
||||
private readonly AIObjectiveManager objectiveManager;
|
||||
|
||||
private float sortTimer;
|
||||
private float crouchRaycastTimer;
|
||||
@@ -29,6 +29,7 @@ namespace Barotrauma
|
||||
private const float FlipInterval = 0.5f;
|
||||
|
||||
public static float HULL_SAFETY_THRESHOLD = 50;
|
||||
private static readonly float characterWaitOnSwitch = 5;
|
||||
|
||||
public readonly HashSet<Hull> UnreachableHulls = new HashSet<Hull>();
|
||||
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
|
||||
@@ -90,8 +91,7 @@ namespace Barotrauma
|
||||
public float CurrentHullSafety { get; private set; } = 100;
|
||||
|
||||
private readonly Dictionary<Character, float> damageDoneByAttacker = new Dictionary<Character, float>();
|
||||
private readonly List<Character> attackers = new List<Character>();
|
||||
|
||||
private readonly HashSet<Character> attackers = new HashSet<Character>();
|
||||
|
||||
public HumanAIController(Character c) : base(c)
|
||||
{
|
||||
@@ -106,11 +106,38 @@ namespace Barotrauma
|
||||
sortTimer = Rand.Range(0f, sortObjectiveInterval);
|
||||
InitProjSpecific();
|
||||
}
|
||||
|
||||
partial void InitProjSpecific();
|
||||
|
||||
private bool freezeAI;
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (DisableCrewAI || Character.IsIncapacitated || Character.Removed) { return; }
|
||||
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)
|
||||
{
|
||||
freezeAI = false;
|
||||
}
|
||||
if (isIncapacitated) { return; }
|
||||
|
||||
base.Update(deltaTime);
|
||||
|
||||
foreach (var values in knownHulls)
|
||||
@@ -164,15 +191,6 @@ namespace Barotrauma
|
||||
}
|
||||
objectiveManager.UpdateObjectives(deltaTime);
|
||||
|
||||
//slowly forget about damage done by attackers
|
||||
foreach (Character enemy in attackers)
|
||||
{
|
||||
if (damageDoneByAttacker[enemy] > 0)
|
||||
{
|
||||
damageDoneByAttacker[enemy] -= deltaTime * 0.01f;
|
||||
}
|
||||
}
|
||||
|
||||
if (reactTimer > 0.0f)
|
||||
{
|
||||
reactTimer -= deltaTime;
|
||||
@@ -277,7 +295,10 @@ namespace Barotrauma
|
||||
{
|
||||
newDir = Direction.Left;
|
||||
}
|
||||
if (Character.SelectedConstruction != null) Character.SelectedConstruction.SecondaryUse(deltaTime, Character);
|
||||
if (Character.SelectedConstruction != null)
|
||||
{
|
||||
Character.SelectedConstruction.SecondaryUse(deltaTime, Character);
|
||||
}
|
||||
}
|
||||
else if (Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater)
|
||||
{
|
||||
@@ -295,53 +316,49 @@ namespace Barotrauma
|
||||
{
|
||||
if (Character.LockHands) { return; }
|
||||
if (ObjectiveManager.CurrentObjective == null) { return; }
|
||||
if (ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>()) { return; }
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.Extinguisher)
|
||||
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
|
||||
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
|
||||
|
||||
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
|
||||
{
|
||||
if (!ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFires>() && !objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>())
|
||||
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
|
||||
Hull targetHull = gotoObjective.GetTargetHull();
|
||||
return gotoObjective.Target != null && targetHull == null ||
|
||||
NeedsDivingGear(targetHull, out _) ||
|
||||
insideSteering && (PathSteering.CurrentPath.HasOutdoorsNodes || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _)));
|
||||
}
|
||||
|
||||
if (isCarrying)
|
||||
{
|
||||
if (findItemState != FindItemState.OtherItem)
|
||||
{
|
||||
var extinguisher = Character.Inventory.FindItemByTag("extinguisher");
|
||||
if (extinguisher != null && Character.HasEquippedItem(extinguisher))
|
||||
if (ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective))
|
||||
{
|
||||
if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
{
|
||||
extinguisher.Drop(Character);
|
||||
}
|
||||
else
|
||||
{
|
||||
findItemState = FindItemState.Extinguisher;
|
||||
if (FindSuitableContainer(extinguisher, out Item targetContainer))
|
||||
{
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, extinguisher, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
extinguisher.Drop(Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
gotoObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
if (!oxygenLow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit || findItemState == FindItemState.DivingMask)
|
||||
|
||||
// Diving gear
|
||||
if (oxygenLow || findItemState != FindItemState.OtherItem)
|
||||
{
|
||||
if (!NeedsDivingGear(Character, Character.CurrentHull, out _))
|
||||
if (!NeedsDivingGear(Character.CurrentHull, out bool needsSuit) || !needsSuit || oxygenLow)
|
||||
{
|
||||
bool shouldKeepTheGearOn = Character.AnimController.HeadInWater
|
||||
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|
||||
|| ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
|
||||
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
|
||||
if (oxygenLow)
|
||||
if (oxygenLow && Character.CurrentHull.Oxygen > 0)
|
||||
{
|
||||
shouldKeepTheGearOn = false;
|
||||
}
|
||||
else if (Character.CurrentHull.Oxygen < CharacterHealth.LowOxygenThreshold)
|
||||
{
|
||||
shouldKeepTheGearOn = true;
|
||||
}
|
||||
bool removeDivingSuit = !shouldKeepTheGearOn;
|
||||
bool takeMaskOff = !shouldKeepTheGearOn;
|
||||
if (!shouldKeepTheGearOn && !oxygenLow)
|
||||
@@ -359,10 +376,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (objective is AIObjectiveGoTo gotoObjective)
|
||||
{
|
||||
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
|
||||
Hull targetHull = gotoObjective.GetTargetHull();
|
||||
bool targetIsOutside = (gotoObjective.Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
|
||||
if (targetIsOutside || NeedsDivingGear(Character, targetHull, out _))
|
||||
if (NeedsDivingGearOnPath(gotoObjective))
|
||||
{
|
||||
removeDivingSuit = false;
|
||||
takeMaskOff = false;
|
||||
@@ -390,81 +404,75 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
|
||||
}
|
||||
if (removeDivingSuit)
|
||||
{
|
||||
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
|
||||
if (divingSuit != null)
|
||||
{
|
||||
if (removeDivingSuit)
|
||||
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
{
|
||||
var divingSuit = Character.Inventory.FindItemByTag("divingsuit");
|
||||
if (divingSuit != null)
|
||||
divingSuit.Drop(Character);
|
||||
}
|
||||
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
|
||||
{
|
||||
findItemState = FindItemState.DivingSuit;
|
||||
if (FindSuitableContainer(divingSuit, out Item targetContainer))
|
||||
{
|
||||
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
divingSuit.Drop(Character);
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
|
||||
{
|
||||
DropIfFailsToContain = false
|
||||
};
|
||||
decontainObjective.Abandoned += () =>
|
||||
{
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
findItemState = FindItemState.DivingSuit;
|
||||
if (FindSuitableContainer(divingSuit, out Item targetContainer))
|
||||
divingSuit.Drop(Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (takeMaskOff)
|
||||
{
|
||||
if (Character.HasEquippedItem(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR))
|
||||
{
|
||||
var mask = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR);
|
||||
if (mask != null)
|
||||
{
|
||||
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
{
|
||||
mask.Drop(Character);
|
||||
}
|
||||
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
|
||||
{
|
||||
findItemState = FindItemState.DivingMask;
|
||||
if (FindSuitableContainer(mask, out Item targetContainer))
|
||||
{
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
|
||||
{
|
||||
DropIfFailsToContain = false
|
||||
};
|
||||
decontainObjective.Abandoned += () =>
|
||||
{
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
divingSuit.Drop(Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
|
||||
{
|
||||
if (takeMaskOff)
|
||||
{
|
||||
if (Character.HasEquippedItem("divingmask"))
|
||||
{
|
||||
var mask = Character.Inventory.FindItemByTag("divingmask");
|
||||
if (mask != null)
|
||||
{
|
||||
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
{
|
||||
mask.Drop(Character);
|
||||
}
|
||||
else
|
||||
{
|
||||
findItemState = FindItemState.DivingMask;
|
||||
if (FindSuitableContainer(mask, out Item targetContainer))
|
||||
{
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
mask.Drop(Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -472,39 +480,37 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
|
||||
}
|
||||
// Other items
|
||||
if (isCarrying) { return; }
|
||||
if (!ObjectiveManager.CurrentObjective.AllowAutomaticItemUnequipping || !ObjectiveManager.GetActiveObjective().AllowAutomaticItemUnequipping) { return; }
|
||||
foreach (var item in Character.Inventory.Items)
|
||||
{
|
||||
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)))
|
||||
{
|
||||
if (!ObjectiveManager.CurrentObjective.UnequipItems || !ObjectiveManager.GetActiveObjective().UnequipItems) { return; }
|
||||
if (ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>()) { return; }
|
||||
foreach (var item in Character.Inventory.Items)
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
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)))
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
|
||||
{
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
findItemState = FindItemState.OtherItem;
|
||||
if (FindSuitableContainer(item, out Item targetContainer))
|
||||
{
|
||||
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 += () => IgnoredItems.Add(targetContainer);
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Drop(Character);
|
||||
}
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
findItemState = FindItemState.OtherItem;
|
||||
item.Drop(Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -518,7 +524,6 @@ namespace Barotrauma
|
||||
None,
|
||||
DivingSuit,
|
||||
DivingMask,
|
||||
Extinguisher,
|
||||
OtherItem
|
||||
}
|
||||
private FindItemState findItemState;
|
||||
@@ -687,16 +692,25 @@ namespace Barotrauma
|
||||
totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
|
||||
}
|
||||
if (totalDamage <= 0) { return; }
|
||||
if (attacker != null)
|
||||
if (Character.IsBot)
|
||||
{
|
||||
if (!damageDoneByAttacker.ContainsKey(attacker))
|
||||
if (attacker != null)
|
||||
{
|
||||
damageDoneByAttacker[attacker] = 0.0f;
|
||||
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.
|
||||
objectiveManager.CreateAutonomousObjectives();
|
||||
objectiveManager.SortObjectives();
|
||||
freezeAI = true;
|
||||
}
|
||||
damageDoneByAttacker[attacker] += totalDamage;
|
||||
attackers.Add(attacker);
|
||||
}
|
||||
if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
|
||||
if (attacker == null || attacker.IsDead || attacker.Removed)
|
||||
{
|
||||
// Don't react on the damage if there's no attacker.
|
||||
@@ -720,76 +734,99 @@ namespace Barotrauma
|
||||
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
|
||||
return;
|
||||
}
|
||||
if (attacker.IsBot)
|
||||
float cumulativeDamage = GetDamageDoneByAttacker(attacker);
|
||||
if (!Character.IsSecurity && attacker.IsBot && !attacker.IsInstigator)
|
||||
{
|
||||
// Don't retaliate on damage done by human ai, because we know it's accidental
|
||||
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime() * 2);
|
||||
if (cumulativeDamage > 1)
|
||||
{
|
||||
// Don't retaliate on damage done by human ai, because we know it's accidental
|
||||
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Character.IsSecurity)
|
||||
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
|
||||
// Inform other NPCs
|
||||
if (cumulativeDamage > 1)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
else
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f);
|
||||
}
|
||||
if (Character.TeamID == Character.TeamType.FriendlyNPC && !Character.TurnedHostileByEvent)
|
||||
{
|
||||
// Inform other characters in the same team
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
if (otherCharacter == Character || otherCharacter.TeamID != Character.TeamID || otherCharacter.IsDead ||
|
||||
otherCharacter.Info?.Job == null ||
|
||||
if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed ||
|
||||
otherCharacter.Info?.Job == null || otherCharacter.TeamID != Character.TeamType.FriendlyNPC ||
|
||||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
|
||||
otherCharacter.TurnedHostileByEvent)
|
||||
otherCharacter.IsInstigator)
|
||||
{
|
||||
continue;
|
||||
continue;
|
||||
}
|
||||
if (!otherHumanAI.IsFriendly(Character)) { continue; }
|
||||
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
|
||||
if (otherCharacter.IsSecurity)
|
||||
{
|
||||
// Alert all the security officers magically
|
||||
float delay = isWitnessing ? GetReactionTime() * 2 : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
|
||||
otherHumanAI.AddCombatObjective(DetermineCombatMode(otherCharacter), attacker, delay);
|
||||
otherHumanAI.AddCombatObjective(DetermineCombatMode(otherCharacter, cumulativeDamage), attacker, delay);
|
||||
}
|
||||
else if (isWitnessing)
|
||||
{
|
||||
var mode = Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat;
|
||||
// Other witnesses retreat to safety
|
||||
otherHumanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime());
|
||||
otherHumanAI.AddCombatObjective(mode, attacker, GetReactionTime());
|
||||
}
|
||||
}
|
||||
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
|
||||
}
|
||||
|
||||
if (attacker.TeamID != Character.TeamID)
|
||||
if (Character.IsBot)
|
||||
{
|
||||
AddCombatObjective(DetermineCombatMode(Character), attacker, GetReactionTime());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't react on minor (accidental) dmg done by characters that are in the same team
|
||||
if (GetDamageDoneByAttacker(attacker) < 10)
|
||||
if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
|
||||
if (Character.IsSecurity)
|
||||
{
|
||||
if (!Character.IsSecurity)
|
||||
if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10)
|
||||
{
|
||||
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime() * 2);
|
||||
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f);
|
||||
}
|
||||
}
|
||||
else if (!Character.IsInstigator && cumulativeDamage > 1)
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f);
|
||||
}
|
||||
if (cumulativeDamage > 1 && attacker.TeamID != Character.TeamID)
|
||||
{
|
||||
// If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay.
|
||||
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddCombatObjective(DetermineCombatMode(Character, dmgThreshold: 20, allowOffensive: false), attacker, GetReactionTime() * 2);
|
||||
bool allowOffensive = HasItem(attacker, "handlocker", out _, requireEquipped: true);
|
||||
if (attackResult.Afflictions.Any(a => a is AfflictionHusk))
|
||||
{
|
||||
cumulativeDamage = 100;
|
||||
}
|
||||
// Don't react on minor (accidental) dmg done by characters that are in the same team
|
||||
if (cumulativeDamage < 10)
|
||||
{
|
||||
if (!Character.IsSecurity && cumulativeDamage > 1)
|
||||
{
|
||||
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 20, allowOffensive: allowOffensive), attacker, GetReactionTime() * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (Character.IsBot)
|
||||
{
|
||||
AddCombatObjective(DetermineCombatMode(Character), attacker);
|
||||
// Non-friendly
|
||||
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker);
|
||||
}
|
||||
|
||||
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float dmgThreshold = 10, bool allowOffensive = true)
|
||||
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, float dmgThreshold = 10, bool allowOffensive = true)
|
||||
{
|
||||
if (!IsFriendly(attacker))
|
||||
{
|
||||
@@ -797,13 +834,38 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GetDamageDoneByAttacker(attacker) > dmgThreshold)
|
||||
if (attacker.TeamID == Character.TeamType.FriendlyNPC)
|
||||
{
|
||||
return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
|
||||
if (c.IsSecurity)
|
||||
{
|
||||
return Character.CombatAction != null ? Character.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.None;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
|
||||
if (Character.IsInstigator)
|
||||
{
|
||||
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
|
||||
}
|
||||
else if (cumulativeDamage > dmgThreshold)
|
||||
{
|
||||
if (c.IsSecurity)
|
||||
{
|
||||
return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
|
||||
}
|
||||
else
|
||||
{
|
||||
return c == Character ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -811,6 +873,8 @@ namespace Barotrauma
|
||||
|
||||
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, bool allowHoldFire = false)
|
||||
{
|
||||
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
|
||||
if (Character.IsDead || Character.IsIncapacitated) { return; }
|
||||
if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective)
|
||||
{
|
||||
// Don't replace offensive mode with something else
|
||||
@@ -896,6 +960,19 @@ namespace Barotrauma
|
||||
SelectedAiTarget = target;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
objectiveManager.SortObjectives();
|
||||
sortTimer = sortObjectiveInterval;
|
||||
float waitDuration = characterWaitOnSwitch;
|
||||
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
|
||||
{
|
||||
waitDuration *= 2;
|
||||
}
|
||||
ObjectiveManager.WaitTimer = waitDuration;
|
||||
}
|
||||
|
||||
private void CheckCrouching(float deltaTime)
|
||||
{
|
||||
crouchRaycastTimer -= deltaTime;
|
||||
@@ -964,13 +1041,13 @@ namespace Barotrauma
|
||||
return targetInventory.TryPutItem(item, targetSlot, false, false, Character);
|
||||
}
|
||||
|
||||
public static bool NeedsDivingGear(Character character, Hull hull, out bool needsSuit)
|
||||
public static bool NeedsDivingGear(Hull hull, out bool needsSuit)
|
||||
{
|
||||
needsSuit = false;
|
||||
if (hull == null ||
|
||||
hull.WaterPercentage > 80 ||
|
||||
(hull.LethalPressure > 0 && character.PressureProtection <= 0) ||
|
||||
(hull.ConnectedGaps.Any() && hull.ConnectedGaps.Max(g => AIObjectiveFixLeaks.GetLeakSeverity(g)) > 60))
|
||||
hull.WaterPercentage > 90 ||
|
||||
hull.LethalPressure > 0 ||
|
||||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f))
|
||||
{
|
||||
needsSuit = true;
|
||||
return true;
|
||||
@@ -987,25 +1064,28 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Check whether the character has a diving suit in usable condition plus some oxygen.
|
||||
/// </summary>
|
||||
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, "divingsuit", out _, "oxygensource", conditionPercentage, requireEquipped: true);
|
||||
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the character has a diving mask in usable condition plus some oxygen.
|
||||
/// </summary>
|
||||
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, "divingmask", out _, "oxygensource", conditionPercentage, requireEquipped: true);
|
||||
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
|
||||
|
||||
public static bool HasItem(Character character, string tagOrIdentifier, out Item item, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false)
|
||||
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)
|
||||
{
|
||||
item = null;
|
||||
matchingItems.Clear();
|
||||
items = matchingItems;
|
||||
if (character == null) { return false; }
|
||||
if (character.Inventory == null) { return false; }
|
||||
item = character.Inventory.FindItemByIdentifier(tagOrIdentifier) ?? character.Inventory.FindItemByTag(tagOrIdentifier);
|
||||
return item != null &&
|
||||
item.ConditionPercentage >= conditionPercentage &&
|
||||
(!requireEquipped || character.HasEquippedItem(item)) &&
|
||||
matchingItems = character.Inventory.FindAllItems(i => i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier), recursive: true, matchingItems);
|
||||
items = matchingItems;
|
||||
return matchingItems.Any(i => i != null &&
|
||||
i.ConditionPercentage >= conditionPercentage &&
|
||||
(!requireEquipped || character.HasEquippedItem(i)) &&
|
||||
(containedTag == null ||
|
||||
(item.ContainedItems != null &&
|
||||
item.ContainedItems.Any(i => i.HasTag(containedTag) && i.ConditionPercentage > conditionPercentage)));
|
||||
(i.OwnInventory?.Items != null &&
|
||||
i.OwnInventory.Items.Any(it => it != null && it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))));
|
||||
}
|
||||
|
||||
public static void ItemTaken(Item item, Character character)
|
||||
@@ -1040,6 +1120,8 @@ namespace Barotrauma
|
||||
otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f);
|
||||
someoneSpoke = true;
|
||||
}
|
||||
// Don't react if the player is taking an extinguisher and there's any fires on the sub -> allow them to use the emergency items
|
||||
if (item.HasTag("fireextinguisher") && character.Submarine.GetHulls(alsoFromConnectedSubs: true).Any(h => h.FireSources.Any())) { continue; }
|
||||
// React if we are security
|
||||
if (!TriggerSecurity(otherHumanAI))
|
||||
{
|
||||
@@ -1240,6 +1322,7 @@ 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);
|
||||
if (!character.NeedsAir)
|
||||
@@ -1409,14 +1492,18 @@ namespace Barotrauma
|
||||
if (target?.Item == null) { return false; }
|
||||
foreach (var c in Character.CharacterList)
|
||||
{
|
||||
if (character != null && c == character) { continue; }
|
||||
if (character?.AIController is HumanAIController humanAi && !humanAi.IsFriendly(c)) { continue; }
|
||||
if (character == null) { continue; }
|
||||
if (c == character) { continue; }
|
||||
if (c.IsDead || c.IsIncapacitated) { continue; }
|
||||
if (c.SelectedConstruction != target.Item) { continue; }
|
||||
if (!IsFriendly(character, c, onlySameTeam: true)) { continue; }
|
||||
operatingCharacter = c;
|
||||
// If the other character is player, don't try to operate
|
||||
if (c.IsRemotePlayer || Character.Controlled == c) { return true; }
|
||||
if (c.IsPlayer) { return true; }
|
||||
if (c.AIController is HumanAIController controllingHumanAi)
|
||||
{
|
||||
Item otherTarget = controllingHumanAi.objectiveManager.GetActiveObjective<AIObjectiveOperateItem>()?.Component.Item ?? c.SelectedConstruction;
|
||||
if (otherTarget != target.Item) { continue; }
|
||||
// If the other character is ordered to operate the item, let him do it
|
||||
if (controllingHumanAi.ObjectiveManager.IsCurrentOrder<AIObjectiveOperateItem>())
|
||||
{
|
||||
|
||||
@@ -111,9 +111,9 @@ namespace Barotrauma
|
||||
IsPathDirty = true;
|
||||
}
|
||||
|
||||
public void SteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
|
||||
public void SteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisiblity = true)
|
||||
{
|
||||
steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter);
|
||||
steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -158,7 +158,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
|
||||
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
|
||||
{
|
||||
Vector2 targetDiff = target - currentTarget;
|
||||
if (currentPath != null && currentPath.Nodes.Any())
|
||||
@@ -172,40 +172,42 @@ namespace Barotrauma
|
||||
targetDiff += subDiff;
|
||||
}
|
||||
}
|
||||
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || targetDiff.LengthSquared() > 1);
|
||||
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || targetDiff.LengthSquared() > 1);
|
||||
//find a new path if one hasn't been found yet or the target is different from the current target
|
||||
if (needsNewPath || findPathTimer < -1.0f)
|
||||
{
|
||||
IsPathDirty = true;
|
||||
if (findPathTimer > 0.0f) { return Vector2.Zero; }
|
||||
currentTarget = target;
|
||||
Vector2 currentPos = host.SimPosition;
|
||||
if (character != null && character.Submarine == null)
|
||||
if (findPathTimer < 0)
|
||||
{
|
||||
var targetHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(target), null, false);
|
||||
if (targetHull != null && targetHull.Submarine != null)
|
||||
currentTarget = target;
|
||||
Vector2 currentPos = host.SimPosition;
|
||||
if (character != null && character.Submarine == null)
|
||||
{
|
||||
currentPos -= targetHull.Submarine.SimPosition;
|
||||
var targetHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(target), null, false);
|
||||
if (targetHull != null && targetHull.Submarine != null)
|
||||
{
|
||||
currentPos -= targetHull.Submarine.SimPosition;
|
||||
}
|
||||
}
|
||||
pathFinder.InsideSubmarine = character.Submarine != null;
|
||||
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
|
||||
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null;
|
||||
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
|
||||
{
|
||||
// It's possible that the current path was calculated from a start point that is no longer valid.
|
||||
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
|
||||
useNewPath = newPath.Cost < currentPath.Cost ||
|
||||
Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
|
||||
}
|
||||
if (useNewPath)
|
||||
{
|
||||
currentPath = newPath;
|
||||
}
|
||||
float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
|
||||
findPathTimer = priority * Rand.Range(1.0f, 1.2f);
|
||||
IsPathDirty = false;
|
||||
return DiffToCurrentNode();
|
||||
}
|
||||
pathFinder.InsideSubmarine = character.Submarine != null;
|
||||
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter);
|
||||
bool useNewPath = currentPath == null || needsNewPath || currentPath.Finished;
|
||||
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
|
||||
{
|
||||
// It's possible that the current path was calculated from a start point that is no longer valid.
|
||||
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
|
||||
useNewPath = newPath.Cost < currentPath.Cost ||
|
||||
Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
|
||||
}
|
||||
if (useNewPath)
|
||||
{
|
||||
currentPath = newPath;
|
||||
}
|
||||
float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
|
||||
findPathTimer = priority * Rand.Range(1.0f, 1.2f);
|
||||
IsPathDirty = false;
|
||||
return DiffToCurrentNode();
|
||||
}
|
||||
|
||||
Vector2 diff = DiffToCurrentNode();
|
||||
@@ -221,7 +223,7 @@ namespace Barotrauma
|
||||
return Vector2.Normalize(diff) * weight;
|
||||
}
|
||||
|
||||
protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight, null, null, null);
|
||||
protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight);
|
||||
|
||||
private Vector2 DiffToCurrentNode()
|
||||
{
|
||||
@@ -260,7 +262,7 @@ namespace Barotrauma
|
||||
}
|
||||
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
|
||||
// Only humanoids can climb ladders
|
||||
bool canClimb = character.AnimController is HumanoidAnimController;
|
||||
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
|
||||
var ladders = GetNextLadder();
|
||||
if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item)
|
||||
{
|
||||
@@ -588,7 +590,7 @@ namespace Barotrauma
|
||||
//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 ||
|
||||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (nextNode.Waypoint.Ladders.Item.NonInteractable || 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
|
||||
{
|
||||
@@ -628,6 +630,7 @@ namespace Barotrauma
|
||||
return penalty;
|
||||
}
|
||||
|
||||
public static float smallRoomSize = 500;
|
||||
public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true)
|
||||
{
|
||||
//steer away from edges of the hull
|
||||
@@ -637,7 +640,7 @@ namespace Barotrauma
|
||||
if (currentHull != null && !inWater)
|
||||
{
|
||||
float roomWidth = currentHull.Rect.Width;
|
||||
if (stayStillInTightSpace && roomWidth < wallAvoidDistance * 4)
|
||||
if (stayStillInTightSpace && roomWidth < Math.Max(wallAvoidDistance * 3, smallRoomSize))
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
@@ -125,8 +125,14 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
attachCooldown -= deltaTime;
|
||||
deattachTimer -= deltaTime;
|
||||
if (attachCooldown > 0)
|
||||
{
|
||||
attachCooldown -= deltaTime;
|
||||
}
|
||||
if (deattachTimer > 0)
|
||||
{
|
||||
deattachTimer -= deltaTime;
|
||||
}
|
||||
|
||||
Vector2 transformedAttachPos = wallAttachPos;
|
||||
if (character.Submarine == null && attachTargetSubmarine != null)
|
||||
@@ -255,6 +261,7 @@ namespace Barotrauma
|
||||
|
||||
private void AttachToBody(PhysicsBody collider, Limb attachLimb, Body targetBody, Vector2 attachPos)
|
||||
{
|
||||
if (attachCooldown > 0) { return; }
|
||||
//already attached to something
|
||||
if (attachJoints.Count > 0)
|
||||
{
|
||||
|
||||
@@ -17,8 +17,7 @@ namespace Barotrauma
|
||||
public virtual bool AllowSubObjectiveSorting => false;
|
||||
|
||||
/// <summary>
|
||||
/// Can there be multiple objective instaces of the same type? Currently multiple instances allowed only for main objectives and the subobjectives of objetive loops.
|
||||
/// In theory, there could be multiple subobjectives of same type for concurrent objectives, but that would make things more complex -> potential issues
|
||||
/// Can there be multiple objective instaces of the same type?
|
||||
/// </summary>
|
||||
public virtual bool AllowMultipleInstances => false;
|
||||
|
||||
@@ -29,7 +28,10 @@ namespace Barotrauma
|
||||
public virtual bool ConcurrentObjectives => false;
|
||||
|
||||
public virtual bool KeepDivingGearOn => false;
|
||||
public virtual bool UnequipItems => false;
|
||||
/// <summary>
|
||||
/// There's a separate property for diving suit and mask: KeepDivingGearOn.
|
||||
/// </summary>
|
||||
public virtual bool AllowAutomaticItemUnequipping => false;
|
||||
public virtual bool AllowOutsideSubmarine => false;
|
||||
public virtual bool AllowInFriendlySubs => false;
|
||||
|
||||
@@ -173,6 +175,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!AllowSubObjectiveSorting) { return; }
|
||||
if (subObjectives.None()) { return; }
|
||||
var previousSubObjective = subObjectives.First();
|
||||
subObjectives.ForEach(so => so.GetPriority());
|
||||
subObjectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
|
||||
if (ConcurrentObjectives)
|
||||
@@ -181,7 +184,13 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
subObjectives.First().SortSubObjectives();
|
||||
var currentSubObjective = subObjectives.First();
|
||||
if (previousSubObjective != currentSubObjective)
|
||||
{
|
||||
previousSubObjective.OnDeselected();
|
||||
currentSubObjective.OnSelected();
|
||||
}
|
||||
currentSubObjective.SortSubObjectives();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +231,7 @@ namespace Barotrauma
|
||||
private void UpdateDevotion(float deltaTime)
|
||||
{
|
||||
var currentObjective = objectiveManager.CurrentObjective;
|
||||
if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this)))
|
||||
if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.FirstOrDefault() == this))
|
||||
{
|
||||
CumulatedDevotion += Devotion * deltaTime;
|
||||
}
|
||||
@@ -327,6 +336,7 @@ namespace Barotrauma
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
subObjectives.Clear();
|
||||
isCompleted = false;
|
||||
hasBeenChecked = false;
|
||||
_abandon = false;
|
||||
@@ -369,8 +379,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (Check())
|
||||
{
|
||||
isCompleted = true;
|
||||
OnCompleted();
|
||||
IsCompleted = true;
|
||||
}
|
||||
}
|
||||
return isCompleted;
|
||||
|
||||
+4
-4
@@ -1,16 +1,16 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
|
||||
{
|
||||
public override string DebugTag => "charge batteries";
|
||||
public override bool UnequipItems => true;
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
private IEnumerable<PowerContainer> batteryList;
|
||||
|
||||
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier)
|
||||
@@ -26,8 +26,7 @@ namespace Barotrauma
|
||||
if (item.Submarine.TeamID != character.TeamID) { return false; }
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
|
||||
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
|
||||
}
|
||||
if (item.ConditionPercentage <= 0) { return false; }
|
||||
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
|
||||
@@ -37,6 +36,7 @@ namespace Barotrauma
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
if (Targets.None()) { return 0; }
|
||||
if (Option == "charge")
|
||||
{
|
||||
return Targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(PowerContainer.aiRechargeTargetRatio - t.RechargeRatio)));
|
||||
|
||||
+177
-73
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FarseerPhysics.Dynamics;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -17,12 +18,11 @@ namespace Barotrauma
|
||||
|
||||
private readonly CombatMode initialMode;
|
||||
|
||||
private float seekWeaponsTimer;
|
||||
private readonly float seekWeaponsInterval = 1;
|
||||
private float checkWeaponsTimer;
|
||||
private readonly float checkWeaponsInterval = 1;
|
||||
private float ignoreWeaponTimer;
|
||||
private readonly float ignoredWeaponsClearTime = 10;
|
||||
|
||||
// Won't (by default) start the offensive with weapons that have lower priority than this
|
||||
private readonly float goodWeaponPriority = 30;
|
||||
|
||||
private readonly float arrestHoldFireTime = 8;
|
||||
@@ -42,7 +42,7 @@ namespace Barotrauma
|
||||
_weapon = value;
|
||||
_weaponComponent = null;
|
||||
hasAimed = false;
|
||||
RemoveSubObjective(ref seekAmmunition);
|
||||
RemoveSubObjective(ref seekAmmunitionObjective);
|
||||
}
|
||||
}
|
||||
private ItemComponent _weaponComponent;
|
||||
@@ -69,13 +69,14 @@ namespace Barotrauma
|
||||
private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
|
||||
private readonly HashSet<Item> ignoredWeapons = new HashSet<Item>();
|
||||
|
||||
private AIObjectiveContainItem seekAmmunition;
|
||||
private AIObjectiveContainItem seekAmmunitionObjective;
|
||||
private AIObjectiveGoTo retreatObjective;
|
||||
private AIObjectiveGoTo followTargetObjective;
|
||||
private AIObjectiveGetItem seekWeaponObjective;
|
||||
|
||||
private Hull retreatTarget;
|
||||
private float coolDownTimer;
|
||||
private IEnumerable<FarseerPhysics.Dynamics.Body> myBodies;
|
||||
private IEnumerable<Body> myBodies;
|
||||
private float aimTimer;
|
||||
|
||||
private bool canSeeTarget;
|
||||
@@ -99,17 +100,27 @@ namespace Barotrauma
|
||||
Defensive,
|
||||
Offensive,
|
||||
Arrest,
|
||||
Retreat
|
||||
Retreat,
|
||||
None
|
||||
}
|
||||
|
||||
public CombatMode Mode { get; private set; }
|
||||
|
||||
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
|
||||
private bool TargetEliminated => Enemy == null || Enemy.Removed || Enemy.IsUnconscious;
|
||||
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
|
||||
private bool EnemyIsClose() => Enemy != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
|
||||
|
||||
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
if (mode == CombatMode.None)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Combat mode == None");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
Enemy = enemy;
|
||||
coolDownTimer = coolDown;
|
||||
findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
|
||||
@@ -145,12 +156,16 @@ namespace Barotrauma
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
ignoreWeaponTimer -= deltaTime;
|
||||
seekWeaponsTimer -= deltaTime;
|
||||
checkWeaponsTimer -= deltaTime;
|
||||
if (ignoreWeaponTimer < 0)
|
||||
{
|
||||
ignoredWeapons.Clear();
|
||||
ignoreWeaponTimer = ignoredWeaponsClearTime;
|
||||
}
|
||||
if (findSafety != null)
|
||||
{
|
||||
findSafety.Priority = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
@@ -164,8 +179,6 @@ namespace Barotrauma
|
||||
return IsEnemyDisabled || (!IsOffensiveOrArrest && coolDownTimer <= 0);
|
||||
}
|
||||
|
||||
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (abortCondition != null && abortCondition())
|
||||
@@ -178,13 +191,13 @@ namespace Barotrauma
|
||||
{
|
||||
coolDownTimer -= deltaTime;
|
||||
}
|
||||
if (seekAmmunition == null)
|
||||
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
{
|
||||
if (Mode != CombatMode.Retreat && TryArm() && !IsEnemyDisabled)
|
||||
{
|
||||
OperateWeapon(deltaTime);
|
||||
}
|
||||
if (!HoldPosition)
|
||||
if (!HoldPosition && seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
{
|
||||
Move(deltaTime);
|
||||
}
|
||||
@@ -193,8 +206,7 @@ namespace Barotrauma
|
||||
case CombatMode.Offensive:
|
||||
if (TargetEliminated && objectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>())
|
||||
{
|
||||
// TODO: enable
|
||||
//character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
|
||||
character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
|
||||
}
|
||||
break;
|
||||
case CombatMode.Arrest:
|
||||
@@ -233,11 +245,11 @@ namespace Barotrauma
|
||||
Weapon = null;
|
||||
return false;
|
||||
}
|
||||
if (seekWeaponsTimer < 0)
|
||||
if (checkWeaponsTimer < 0)
|
||||
{
|
||||
seekWeaponsTimer = seekWeaponsInterval;
|
||||
checkWeaponsTimer = checkWeaponsInterval;
|
||||
// First go through all weapons and try to reload without seeking ammunition
|
||||
var allWeapons = GetAllWeapons();
|
||||
var allWeapons = FindWeaponsFromInventory();
|
||||
while (allWeapons.Any())
|
||||
{
|
||||
Weapon = GetWeapon(allWeapons, out _weaponComponent);
|
||||
@@ -273,12 +285,12 @@ namespace Barotrauma
|
||||
if (Weapon == null)
|
||||
{
|
||||
// No weapon found with the conditions above. Try again, now let's try to seek ammunition too
|
||||
Weapon = GetWeapon(out _weaponComponent);
|
||||
Weapon = FindWeapon(out _weaponComponent);
|
||||
if (Weapon != null)
|
||||
{
|
||||
if (!CheckWeapon(seekAmmo: true))
|
||||
{
|
||||
if (seekAmmunition != null)
|
||||
if (seekAmmunitionObjective != null)
|
||||
{
|
||||
// No loaded weapon, but we are trying to seek ammunition.
|
||||
return false;
|
||||
@@ -290,9 +302,58 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Weapon == null)
|
||||
bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != Character.TeamType.FriendlyNPC && IsOffensiveOrArrest;
|
||||
if (!isAllowedToSeekWeapons)
|
||||
{
|
||||
Mode = CombatMode.Retreat;
|
||||
if (WeaponComponent == null)
|
||||
{
|
||||
Mode = CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
else if (seekAmmunitionObjective == null && (WeaponComponent == null || WeaponComponent.CombatPriority < goodWeaponPriority))
|
||||
{
|
||||
// Poor weapon equipped -> try to find better.
|
||||
RemoveSubObjective(ref seekAmmunitionObjective);
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
RemoveSubObjective(ref followTargetObjective);
|
||||
TryAddSubObjective(ref seekWeaponObjective,
|
||||
constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false)
|
||||
{
|
||||
GetItemPriority = i =>
|
||||
{
|
||||
if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
|
||||
if (i.IsOwnedBy(character)) { return 0; }
|
||||
var mw = i.GetComponent<MeleeWeapon>();
|
||||
var rw = i.GetComponent<RangedWeapon>();
|
||||
float priority = 0;
|
||||
if (mw != null)
|
||||
{
|
||||
priority = mw.CombatPriority / 100;
|
||||
}
|
||||
else if (rw != null)
|
||||
{
|
||||
priority = rw.CombatPriority / 100;
|
||||
}
|
||||
if (i.HasTag("stunner"))
|
||||
{
|
||||
if (Mode == CombatMode.Arrest)
|
||||
{
|
||||
priority *= 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
priority /= 2;
|
||||
}
|
||||
}
|
||||
return priority;
|
||||
}
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref seekWeaponObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref seekWeaponObjective);
|
||||
Mode = CombatMode.Retreat;
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -342,32 +403,20 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private Item GetWeapon(out ItemComponent weaponComponent) => GetWeapon(GetAllWeapons(), out weaponComponent);
|
||||
private Item FindWeapon(out ItemComponent weaponComponent) => GetWeapon(FindWeaponsFromInventory(), out weaponComponent);
|
||||
|
||||
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
|
||||
{
|
||||
weaponComponent = null;
|
||||
float bestPriority = 0;
|
||||
float lethalDmg = -1;
|
||||
bool enemyIsClose = EnemyIsClose();
|
||||
foreach (var weapon in weaponList)
|
||||
{
|
||||
// By default, the bots won't go offensive with bad weapons, unless they are close to the enemy or ordered to fight enemies.
|
||||
// NPC characters ignore this check.
|
||||
if ((initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest) && character.TeamID != Character.TeamType.FriendlyNPC)
|
||||
{
|
||||
if (!objectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() && !EnemyIsClose())
|
||||
{
|
||||
if (weapon.CombatPriority < goodWeaponPriority)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float priority = weapon.CombatPriority;
|
||||
if (!IsLoaded(weapon))
|
||||
{
|
||||
if (weapon is RangedWeapon && EnemyIsClose())
|
||||
if (weapon is RangedWeapon && enemyIsClose)
|
||||
{
|
||||
// Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo).
|
||||
continue;
|
||||
@@ -420,6 +469,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (weapon is MeleeWeapon && weapon.Item.HasTag("stunner") && !CanMeleeStunnerStun(weapon))
|
||||
{
|
||||
Attack attack = GetAttackDefinition(weapon);
|
||||
priority = attack?.GetTotalDamage() ?? priority / 2;
|
||||
}
|
||||
if (priority > bestPriority)
|
||||
{
|
||||
weaponComponent = weapon;
|
||||
@@ -449,9 +503,7 @@ namespace Barotrauma
|
||||
}
|
||||
return weaponComponent.Item;
|
||||
|
||||
bool EnemyIsClose() => character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
|
||||
|
||||
Attack GetAttackDefinition(ItemComponent weapon)
|
||||
static Attack GetAttackDefinition(ItemComponent weapon)
|
||||
{
|
||||
Attack attack = null;
|
||||
if (weapon is MeleeWeapon meleeWeapon)
|
||||
@@ -465,7 +517,7 @@ namespace Barotrauma
|
||||
return attack;
|
||||
}
|
||||
|
||||
float GetLethalDamage(ItemComponent weapon)
|
||||
static float GetLethalDamage(ItemComponent weapon)
|
||||
{
|
||||
float lethalDmg = 0;
|
||||
Attack attack = GetAttackDefinition(weapon);
|
||||
@@ -499,25 +551,38 @@ namespace Barotrauma
|
||||
});
|
||||
return attack.Stun + afflictionsStun + effectsStun;
|
||||
}
|
||||
|
||||
bool CanMeleeStunnerStun(ItemComponent weapon)
|
||||
{
|
||||
// If there's an item container that takes a battery,
|
||||
// assume that it's required for the stun effect
|
||||
// as we can't check the status effect conditions here.
|
||||
var mobileBatteryTag = "mobilebattery";
|
||||
var containers = weapon.Item.Components.Where(ic => ic is ItemContainer container &&
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private HashSet<ItemComponent> GetAllWeapons()
|
||||
private HashSet<ItemComponent> FindWeaponsFromInventory()
|
||||
{
|
||||
weapons.Clear();
|
||||
foreach (var item in character.Inventory.Items)
|
||||
{
|
||||
if (item == null) { continue; }
|
||||
if (ignoredWeapons.Contains(item)) { continue; }
|
||||
SeekWeapons(item, weapons);
|
||||
GetWeapons(item, weapons);
|
||||
if (item.OwnInventory != null)
|
||||
{
|
||||
item.OwnInventory.Items.ForEach(i => SeekWeapons(i, weapons));
|
||||
item.OwnInventory.Items.ForEach(i => GetWeapons(i, weapons));
|
||||
}
|
||||
}
|
||||
return weapons;
|
||||
}
|
||||
|
||||
private void SeekWeapons(Item item, ICollection<ItemComponent> weaponList)
|
||||
private void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
|
||||
{
|
||||
if (item == null) { return; }
|
||||
foreach (var component in item.Components)
|
||||
@@ -571,7 +636,7 @@ namespace Barotrauma
|
||||
private void Retreat(float deltaTime)
|
||||
{
|
||||
RemoveFollowTarget();
|
||||
RemoveSubObjective(ref seekAmmunition);
|
||||
RemoveSubObjective(ref seekAmmunitionObjective);
|
||||
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
|
||||
{
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
@@ -611,6 +676,12 @@ namespace Barotrauma
|
||||
|
||||
private void Engage()
|
||||
{
|
||||
if (WeaponComponent == null)
|
||||
{
|
||||
RemoveFollowTarget();
|
||||
SteeringManager.Reset();
|
||||
return;
|
||||
}
|
||||
if (character.LockHands || Enemy == null)
|
||||
{
|
||||
Mode = CombatMode.Retreat;
|
||||
@@ -619,7 +690,8 @@ namespace Barotrauma
|
||||
}
|
||||
retreatTarget = null;
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
RemoveSubObjective(ref seekAmmunition);
|
||||
RemoveSubObjective(ref seekAmmunitionObjective);
|
||||
RemoveSubObjective(ref seekWeaponObjective);
|
||||
if (followTargetObjective != null && followTargetObjective.Target != Enemy)
|
||||
{
|
||||
RemoveFollowTarget();
|
||||
@@ -639,7 +711,7 @@ namespace Barotrauma
|
||||
if (followTargetObjective == null) { return; }
|
||||
if (Mode == CombatMode.Arrest && Enemy.Stun > 2)
|
||||
{
|
||||
if (HumanAIController.HasItem(character, "handlocker", out Item handCuffs))
|
||||
if (HumanAIController.HasItem(character, "handlocker", out _))
|
||||
{
|
||||
if (!arrestingRegistered)
|
||||
{
|
||||
@@ -650,16 +722,19 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
{
|
||||
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs");
|
||||
if (prefab != null)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInOutpost = true);
|
||||
}
|
||||
}
|
||||
RemoveFollowTarget();
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
}
|
||||
else if (WeaponComponent == null)
|
||||
{
|
||||
RemoveFollowTarget();
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
else
|
||||
if (followTargetObjective != null)
|
||||
{
|
||||
followTargetObjective.CloseEnough =
|
||||
WeaponComponent is RangedWeapon ? 1000 :
|
||||
@@ -682,8 +757,9 @@ namespace Barotrauma
|
||||
|
||||
private void OnArrestTargetReached()
|
||||
{
|
||||
if (HumanAIController.HasItem(character, "handlocker", out Item handCuffs) && Enemy.Stun > 0 && character.CanInteractWith(Enemy))
|
||||
if (HumanAIController.HasItem(character, "handlocker", out IEnumerable<Item> matchingItems) && Enemy.Stun > 0 && character.CanInteractWith(Enemy))
|
||||
{
|
||||
var handCuffs = matchingItems.First();
|
||||
if (HumanAIController.TryToMoveItem(handCuffs, Enemy.Inventory))
|
||||
{
|
||||
handCuffs.Equip(Enemy);
|
||||
@@ -704,8 +780,7 @@ namespace Barotrauma
|
||||
character.Inventory.TryPutItem(item, character, new List<InvSlotType>() { InvSlotType.Any });
|
||||
}
|
||||
}
|
||||
// TODO: enable
|
||||
//character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f);
|
||||
character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f);
|
||||
IsCompleted = true;
|
||||
}
|
||||
}
|
||||
@@ -717,18 +792,19 @@ namespace Barotrauma
|
||||
{
|
||||
retreatTarget = null;
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
RemoveSubObjective(ref seekWeaponObjective);
|
||||
RemoveFollowTarget();
|
||||
TryAddSubObjective(ref seekAmmunition,
|
||||
TryAddSubObjective(ref seekAmmunitionObjective,
|
||||
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
|
||||
{
|
||||
targetItemCount = Weapon.GetComponent<ItemContainer>().Capacity,
|
||||
checkInventory = false
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref seekAmmunition),
|
||||
onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
RemoveSubObjective(ref seekAmmunition);
|
||||
RemoveSubObjective(ref seekAmmunitionObjective);
|
||||
ignoredWeapons.Add(Weapon);
|
||||
Weapon = null;
|
||||
});
|
||||
@@ -742,7 +818,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (WeaponComponent == null) { return false; }
|
||||
if (!WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return false; }
|
||||
var containedItems = Weapon.ContainedItems;
|
||||
var containedItems = Weapon.OwnInventory?.Items;
|
||||
if (containedItems == null) { return true; }
|
||||
// Drop empty ammo
|
||||
foreach (Item containedItem in containedItems)
|
||||
{
|
||||
@@ -757,7 +834,7 @@ namespace Barotrauma
|
||||
string[] ammunitionIdentifiers = null;
|
||||
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
|
||||
{
|
||||
ammunition = containedItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it));
|
||||
ammunition = containedItems.FirstOrDefault(it => it != null && it.Condition > 0 && requiredItem.MatchesItem(it));
|
||||
if (ammunition != null)
|
||||
{
|
||||
// Ammunition still remaining
|
||||
@@ -831,36 +908,50 @@ namespace Barotrauma
|
||||
aimTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 0) { return; }
|
||||
if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 1) { return; }
|
||||
if (holdFireCondition != null && holdFireCondition()) { return; }
|
||||
float sqrDist = Vector2.DistanceSquared(character.Position, Enemy.Position);
|
||||
if (!character.IsFacing(Enemy.WorldPosition))
|
||||
{
|
||||
aimTimer = Rand.Range(1f, 1.5f);
|
||||
return;
|
||||
}
|
||||
if (WeaponComponent is MeleeWeapon meleeWeapon)
|
||||
{
|
||||
bool closeEnough = true;
|
||||
float sqrRange = meleeWeapon.Range * meleeWeapon.Range;
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
if (sqrDist > sqrRange) { return; }
|
||||
if (sqrDist > sqrRange)
|
||||
{
|
||||
closeEnough = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's possible that the center point of the creature is out of reach, but we could still hit the character.
|
||||
float xDiff = Math.Abs(Enemy.WorldPosition.X - character.WorldPosition.X);
|
||||
if (xDiff > meleeWeapon.Range) { return; }
|
||||
if (xDiff > meleeWeapon.Range)
|
||||
{
|
||||
closeEnough = false;
|
||||
}
|
||||
float yDiff = Math.Abs(Enemy.WorldPosition.Y - character.WorldPosition.Y);
|
||||
if (yDiff > Math.Max(meleeWeapon.Range, 100)) { return; }
|
||||
if (Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25)
|
||||
if (yDiff > Math.Max(meleeWeapon.Range, 100))
|
||||
{
|
||||
closeEnough = false;
|
||||
}
|
||||
if (closeEnough && Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25)
|
||||
{
|
||||
// The target is probably knocked down? -> try to reach it by crouching.
|
||||
HumanAIController.AnimController.Crouching = true;
|
||||
}
|
||||
}
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
Weapon.Use(deltaTime, character);
|
||||
if (closeEnough)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
Weapon.Use(deltaTime, character);
|
||||
}
|
||||
else if (!character.IsFacing(Enemy.WorldPosition))
|
||||
{
|
||||
// Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around.
|
||||
aimTimer = Rand.Range(1f, 1.5f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -916,6 +1007,19 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
hasAimed = false;
|
||||
isLethalWeapon = false;
|
||||
canSeeTarget = false;
|
||||
seekWeaponObjective = null;
|
||||
seekAmmunitionObjective = null;
|
||||
retreatObjective = null;
|
||||
followTargetObjective = null;
|
||||
retreatTarget = null;
|
||||
}
|
||||
|
||||
//private float CalculateEnemyStrength()
|
||||
//{
|
||||
// float enemyStrength = 0;
|
||||
|
||||
+20
-17
@@ -1,5 +1,4 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -30,6 +29,7 @@ namespace Barotrauma
|
||||
private readonly HashSet<Item> containedItems = new HashSet<Item>();
|
||||
|
||||
public bool AllowToFindDivingGear { get; set; } = true;
|
||||
public bool AllowDangerousPressure { get; set; }
|
||||
public float ConditionLevel { get; set; }
|
||||
public bool Equip { get; set; }
|
||||
public bool RemoveEmpty { get; set; } = true;
|
||||
@@ -53,13 +53,17 @@ namespace Barotrauma
|
||||
{
|
||||
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
|
||||
}
|
||||
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
if (container == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
if (item != null)
|
||||
{
|
||||
return container.Inventory.Items.Contains(item);
|
||||
@@ -143,8 +147,8 @@ namespace Barotrauma
|
||||
DialogueIdentifier = "dialogcannotreachtarget",
|
||||
TargetName = container.Item.Name
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective));
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective));
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -156,7 +160,9 @@ namespace Barotrauma
|
||||
GetItemPriority = GetItemPriority,
|
||||
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
|
||||
ignoredItems = containedItems,
|
||||
AllowToFindDivingGear = this.AllowToFindDivingGear
|
||||
AllowToFindDivingGear = AllowToFindDivingGear,
|
||||
AllowDangerousPressure = AllowDangerousPressure,
|
||||
TargetCondition = ConditionLevel
|
||||
}, onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
@@ -166,20 +172,17 @@ namespace Barotrauma
|
||||
{
|
||||
containedItems.Add(getItemObjective.TargetItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (container.Inventory.FindItem(i => CheckItem(i), recursive: false) != null)
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
RemoveSubObjective(ref getItemObjective);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
getItemObjective = null;
|
||||
goToObjective = null;
|
||||
containedItems.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,5 +1,4 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
@@ -130,5 +129,12 @@ namespace Barotrauma
|
||||
IsCompleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
goToObjective = null;
|
||||
containObjective = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -60,7 +60,7 @@ namespace Barotrauma
|
||||
private float sinTime;
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
var extinguisherItem = character.Inventory.FindItemByIdentifier("fireextinguisher") ?? character.Inventory.FindItemByTag("fireextinguisher");
|
||||
var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher");
|
||||
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
|
||||
{
|
||||
TryAddSubObjective(ref getExtinguisherObjective, () =>
|
||||
@@ -147,5 +147,14 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
getExtinguisherObjective = null;
|
||||
gotoObjective = null;
|
||||
useExtinquisherTimer = 0;
|
||||
sinTime = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-6
@@ -1,7 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -14,7 +13,7 @@ namespace Barotrauma
|
||||
|
||||
protected override bool Filter(Hull hull) => IsValidTarget(hull, character);
|
||||
|
||||
protected override float TargetEvaluation() => objectiveManager.CurrentObjective == this ? 100 : Targets.Sum(t => GetFireSeverity(t));
|
||||
protected override float TargetEvaluation() => Targets.Sum(t => GetFireSeverity(t));
|
||||
|
||||
public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X);
|
||||
|
||||
@@ -31,11 +30,22 @@ namespace Barotrauma
|
||||
if (hull == null) { return false; }
|
||||
if (hull.FireSources.None()) { return false; }
|
||||
if (hull.Submarine == null) { return false; }
|
||||
if (hull.Submarine.TeamID != character.TeamID) { return false; }
|
||||
if (character.Submarine != null)
|
||||
if (character.Submarine == null) { return false; }
|
||||
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { return false; }
|
||||
if (character.AIController is HumanAIController humanAI)
|
||||
{
|
||||
if (hull.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; }
|
||||
if (hull.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
if (humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>())
|
||||
{
|
||||
// For orders, allow targets in the current sub (for example if the bot is inside an outpost or a wreck)
|
||||
if (hull.Submarine != character.Submarine) { return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+3
-4
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -9,7 +10,6 @@ namespace Barotrauma
|
||||
protected override float IgnoreListClearInterval => 30;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
|
||||
|
||||
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Barotrauma
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
// TODO: sorting criteria
|
||||
return 100;
|
||||
return Targets.None() ? 0 : 100;
|
||||
}
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Character target)
|
||||
@@ -56,8 +56,7 @@ namespace Barotrauma
|
||||
if (target.CurrentHull == null) { return false; }
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
|
||||
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+67
-68
@@ -1,5 +1,4 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -9,21 +8,24 @@ namespace Barotrauma
|
||||
public override string DebugTag => $"find diving gear ({gearTag})";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
|
||||
private readonly string gearTag;
|
||||
private readonly string fallbackTag;
|
||||
|
||||
private AIObjectiveGetItem getDivingGear;
|
||||
private AIObjectiveContainItem getOxygen;
|
||||
private Item targetItem;
|
||||
|
||||
public static float lowOxygenThreshold = 10;
|
||||
public static float MIN_OXYGEN = 10;
|
||||
public static string HEAVY_DIVING_GEAR = "heavydiving";
|
||||
public static string LIGHT_DIVING_GEAR = "lightdiving";
|
||||
public static string OXYGEN_SOURCE = "oxygensource";
|
||||
|
||||
protected override bool Check() => HumanAIController.HasItem(character, gearTag, out _, "oxygensource", requireEquipped: true) || HumanAIController.HasItem(character, fallbackTag, out _, "oxygensource", requireEquipped: true);
|
||||
protected override bool Check() => targetItem != null && character.HasEquippedItem(targetItem);
|
||||
|
||||
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
gearTag = needDivingSuit ? "divingsuit" : "divingmask";
|
||||
fallbackTag = needDivingSuit ? "divingsuit" : "diving";
|
||||
gearTag = needsDivingSuit ? HEAVY_DIVING_GEAR : LIGHT_DIVING_GEAR;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
@@ -33,98 +35,95 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
var item = character.Inventory.FindItemByIdentifier(gearTag, true) ?? character.Inventory.FindItemByTag(gearTag, true);
|
||||
if (item == null && fallbackTag != gearTag)
|
||||
{
|
||||
item = character.Inventory.FindItemByTag(fallbackTag, true);
|
||||
}
|
||||
if (item == null || !character.HasEquippedItem(item))
|
||||
targetItem = character.Inventory.FindItemByTag(gearTag, true);
|
||||
if (targetItem == null || !character.HasEquippedItem(targetItem))
|
||||
{
|
||||
TryAddSubObjective(ref getDivingGear, () =>
|
||||
{
|
||||
if (item == null)
|
||||
if (targetItem == null)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
|
||||
}
|
||||
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { AllowToFindDivingGear = false };
|
||||
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
|
||||
{
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true
|
||||
};
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref getDivingGear));
|
||||
}
|
||||
else
|
||||
{
|
||||
var containedItems = item.ContainedItems;
|
||||
if (containedItems == null)
|
||||
if (!DropEmptyTanks(character, targetItem, out Item[] containedItems))
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory");
|
||||
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + targetItem + "\" has no proper inventory");
|
||||
#endif
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
// Drop empty tanks
|
||||
foreach (Item containedItem in containedItems)
|
||||
if (containedItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > MIN_OXYGEN))
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
// No valid oxygen source loaded.
|
||||
// Seek oxygen that has min 10% condition left.
|
||||
TryAddSubObjective(ref getOxygen, () =>
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
}
|
||||
if (containedItems.None(it => it.HasTag("oxygensource") && it.Condition > lowOxygenThreshold))
|
||||
{
|
||||
var oxygenTank = character.Inventory.FindItemByTag("oxygensource", true);
|
||||
if (oxygenTank != null)
|
||||
{
|
||||
var container = item.GetComponent<ItemContainer>();
|
||||
if (container.Item.ParentInventory == character.Inventory)
|
||||
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 (!container.Inventory.CanBePut(oxygenTank))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
character.Inventory.RemoveItem(oxygenTank);
|
||||
if (!container.Inventory.TryPutItem(oxygenTank, null))
|
||||
{
|
||||
oxygenTank.Drop(character);
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
container.Combine(oxygenTank, character);
|
||||
}
|
||||
}
|
||||
else
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
ConditionLevel = MIN_OXYGEN
|
||||
};
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
// Seek oxygen that has min 10% condition left
|
||||
// Try to seek any oxygen sources.
|
||||
TryAddSubObjective(ref getOxygen, () =>
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
|
||||
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
|
||||
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
{
|
||||
AllowToFindDivingGear = false,
|
||||
ConditionLevel = lowOxygenThreshold
|
||||
AllowDangerousPressure = true,
|
||||
ConditionLevel = 0
|
||||
};
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
// Try to seek any oxygen sources
|
||||
TryAddSubObjective(ref getOxygen, () =>
|
||||
{
|
||||
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
|
||||
{
|
||||
AllowToFindDivingGear = false,
|
||||
ConditionLevel = 0
|
||||
};
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref getOxygen));
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref getOxygen));
|
||||
}
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref getOxygen));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
containedItems = target.OwnInventory?.Items;
|
||||
if (containedItems == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach (Item containedItem in containedItems)
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
{
|
||||
containedItem.Drop(actor);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
getDivingGear = null;
|
||||
getOxygen = null;
|
||||
targetItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+84
-40
@@ -14,7 +14,8 @@ namespace Barotrauma
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
public override bool ConcurrentObjectives => true;
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); }
|
||||
|
||||
// TODO: expose?
|
||||
const float priorityIncrease = 100;
|
||||
@@ -48,7 +49,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (HumanAIController.NeedsDivingGear(character, character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
|
||||
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
|
||||
{
|
||||
Priority = 100;
|
||||
}
|
||||
@@ -64,6 +65,14 @@ namespace Barotrauma
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (retryTimer > 0)
|
||||
{
|
||||
retryTimer -= deltaTime;
|
||||
if (retryTimer <= 0)
|
||||
{
|
||||
retryCounter = 0;
|
||||
}
|
||||
}
|
||||
if (resetPriority)
|
||||
{
|
||||
Priority = 0;
|
||||
@@ -92,39 +101,56 @@ namespace Barotrauma
|
||||
|
||||
private Hull currentSafeHull;
|
||||
private Hull previousSafeHull;
|
||||
private bool cannotFindSafeHull;
|
||||
private bool cannotFindDivingGear;
|
||||
private readonly int findDivingGearAttempts = 2;
|
||||
private int retryCounter;
|
||||
private readonly float retryResetTime = 5;
|
||||
private float retryTimer;
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
var currentHull = character.CurrentHull;
|
||||
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0;
|
||||
if (!dangerousPressure)
|
||||
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0;
|
||||
if (!character.LockHands && (!dangerousPressure || cannotFindSafeHull))
|
||||
{
|
||||
// Don't try to seek diving gear if the pressure is dangerous. Just get out.
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit);
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
|
||||
bool needsEquipment = false;
|
||||
if (needsDivingSuit)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
|
||||
}
|
||||
else if (needsDivingGear)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
|
||||
}
|
||||
if (needsEquipment && divingGearObjective == null && !character.LockHands)
|
||||
if (needsEquipment)
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref divingGearObjective,
|
||||
if (cannotFindDivingGear && retryCounter < findDivingGearAttempts)
|
||||
{
|
||||
retryTimer = retryResetTime;
|
||||
retryCounter++;
|
||||
needsDivingSuit = !needsDivingSuit;
|
||||
RemoveSubObjective(ref divingGearObjective);
|
||||
}
|
||||
if (divingGearObjective == null)
|
||||
{
|
||||
cannotFindDivingGear = false;
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref divingGearObjective,
|
||||
constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
|
||||
onAbandon: () =>
|
||||
{
|
||||
searchHullTimer = Math.Min(1, searchHullTimer);
|
||||
// Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
|
||||
},
|
||||
cannotFindDivingGear = true;
|
||||
// Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
resetPriority = true;
|
||||
searchHullTimer = Math.Min(1, searchHullTimer);
|
||||
RemoveSubObjective(ref divingGearObjective);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
|
||||
@@ -142,6 +168,7 @@ namespace Barotrauma
|
||||
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
|
||||
previousSafeHull = currentSafeHull;
|
||||
currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC);
|
||||
cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _);
|
||||
if (currentSafeHull == null)
|
||||
{
|
||||
currentSafeHull = previousSafeHull;
|
||||
@@ -153,35 +180,38 @@ namespace Barotrauma
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
}
|
||||
TryAddSubObjective(ref goToObjective,
|
||||
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
|
||||
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
|
||||
{
|
||||
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
|
||||
HumanAIController.NeedsDivingGear(currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
|
||||
{
|
||||
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
|
||||
},
|
||||
onCompleted: () =>
|
||||
resetPriority = true;
|
||||
searchHullTimer = Math.Min(1, searchHullTimer);
|
||||
}
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
if (cannotFindDivingGear)
|
||||
{
|
||||
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
|
||||
HumanAIController.NeedsDivingGear(character, currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
|
||||
{
|
||||
resetPriority = true;
|
||||
searchHullTimer = Math.Min(1, searchHullTimer);
|
||||
}
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
// If diving gear objective failed, let's reset it here.
|
||||
RemoveSubObjective(ref divingGearObjective);
|
||||
},
|
||||
onAbandon: () =>
|
||||
}
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
// Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
|
||||
// If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
|
||||
if (currentHull != null && goToObjective != null)
|
||||
{
|
||||
// Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
|
||||
// If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
|
||||
if (currentHull != null && goToObjective != null)
|
||||
if (goToObjective.Target is Hull hull)
|
||||
{
|
||||
if (goToObjective.Target is Hull hull)
|
||||
{
|
||||
HumanAIController.UnreachableHulls.Add(hull);
|
||||
}
|
||||
HumanAIController.UnreachableHulls.Add(hull);
|
||||
}
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
});
|
||||
}
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -194,12 +224,14 @@ namespace Barotrauma
|
||||
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
|
||||
// -> attempt to manually steer away from hazards
|
||||
Vector2 escapeVel = Vector2.Zero;
|
||||
// TODO: optimize
|
||||
foreach (FireSource fireSource in HumanAIController.VisibleHulls.SelectMany(h => h.FireSources))
|
||||
foreach (Hull hull in HumanAIController.VisibleHulls)
|
||||
{
|
||||
Vector2 dir = character.Position - fireSource.Position;
|
||||
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
|
||||
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
|
||||
foreach (FireSource fireSource in hull.FireSources)
|
||||
{
|
||||
Vector2 dir = character.Position - fireSource.Position;
|
||||
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
|
||||
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
|
||||
}
|
||||
}
|
||||
foreach (Character enemy in Character.CharacterList)
|
||||
{
|
||||
@@ -335,5 +367,17 @@ namespace Barotrauma
|
||||
}
|
||||
return bestHull;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
goToObjective = null;
|
||||
divingGearObjective = null;
|
||||
currentSafeHull = null;
|
||||
previousSafeHull = null;
|
||||
retryCounter = 0;
|
||||
cannotFindDivingGear = false;
|
||||
cannotFindSafeHull = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-11
@@ -20,12 +20,12 @@ namespace Barotrauma
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
private AIObjectiveOperateItem operateObjective;
|
||||
|
||||
public bool IgnoreSeverityAndDistance { get; private set; }
|
||||
public readonly bool isPriority;
|
||||
|
||||
public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool ignoreSeverityAndDistance = false) : base (character, objectiveManager, priorityModifier)
|
||||
public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false) : base (character, objectiveManager, priorityModifier)
|
||||
{
|
||||
Leak = leak;
|
||||
IgnoreSeverityAndDistance = ignoreSeverityAndDistance;
|
||||
this.isPriority = isPriority;
|
||||
}
|
||||
|
||||
protected override bool Check() => Leak.Open <= 0 || Leak.Removed;
|
||||
@@ -41,15 +41,21 @@ namespace Barotrauma
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeak>()?.Leak == Leak))
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
|
||||
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
|
||||
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
|
||||
float distanceFactor = IgnoreSeverityAndDistance || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
|
||||
float severity = IgnoreSeverityAndDistance ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
|
||||
float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
|
||||
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
|
||||
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
|
||||
float reduction = isPriority ? 1 : 2;
|
||||
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
}
|
||||
@@ -68,7 +74,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
var containedItems = weldingTool.ContainedItems;
|
||||
var containedItems = weldingTool.OwnInventory?.Items;
|
||||
if (containedItems == null)
|
||||
{
|
||||
#if DEBUG
|
||||
@@ -86,7 +92,7 @@ namespace Barotrauma
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
}
|
||||
if (containedItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
|
||||
if (containedItems.None(i => i != null && i.HasTag("weldingfuel") && i.Condition > 0.0f))
|
||||
{
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
|
||||
onAbandon: () => Abandon = true,
|
||||
@@ -130,11 +136,10 @@ namespace Barotrauma
|
||||
{
|
||||
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager)
|
||||
{
|
||||
// Disabled for now
|
||||
//AllowGoingOutside = !Leak.IsRoomToRoom && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() && HumanAIController.HasDivingSuit(character, conditionPercentage: 50),
|
||||
CloseEnough = reach,
|
||||
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null,
|
||||
TargetName = Leak.FlowTargetHull?.DisplayName
|
||||
TargetName = Leak.FlowTargetHull?.DisplayName,
|
||||
CheckVisibility = false
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -153,5 +158,14 @@ namespace Barotrauma
|
||||
onCompleted: () => RemoveSubObjective(ref gotoObjective));
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
getWeldingTool = null;
|
||||
refuelObjective = null;
|
||||
gotoObjective = null;
|
||||
operateObjective = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-6
@@ -37,11 +37,9 @@ namespace Barotrauma
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
|
||||
int totalLeaks = Targets.Count();
|
||||
if (totalLeaks == 0) { return 0; }
|
||||
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
|
||||
int leaks = totalLeaks - secondaryLeaks;
|
||||
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
|
||||
bool anyFixers = otherFixers > 0;
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
{
|
||||
@@ -50,6 +48,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
|
||||
int leaks = totalLeaks - secondaryLeaks;
|
||||
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1;
|
||||
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
|
||||
{
|
||||
@@ -62,7 +62,7 @@ namespace Barotrauma
|
||||
|
||||
protected override IEnumerable<Gap> GetList() => Gap.GapList;
|
||||
protected override AIObjective ObjectiveConstructor(Gap gap)
|
||||
=> new AIObjectiveFixLeak(gap, character, objectiveManager, priorityModifier: PriorityModifier, ignoreSeverityAndDistance: gap.FlowTargetHull == PrioritizedHull);
|
||||
=> new AIObjectiveFixLeak(gap, character, objectiveManager, priorityModifier: PriorityModifier, isPriority: gap.FlowTargetHull == PrioritizedHull);
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Gap target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectiveFixLeaks, Gap>(character, target);
|
||||
@@ -75,8 +75,7 @@ namespace Barotrauma
|
||||
if (gap.Submarine.TeamID != character.TeamID) { return false; }
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
if (gap.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; }
|
||||
if (!character.Submarine.IsConnectedTo(gap.Submarine)) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+35
-49
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -17,10 +16,9 @@ namespace Barotrauma
|
||||
public Func<Item, float> GetItemPriority;
|
||||
public Func<Item, bool> ItemFilter;
|
||||
public float TargetCondition { get; set; } = 1;
|
||||
public bool AllowDangerousPressure { get; set; }
|
||||
|
||||
//can be either tags or identifiers
|
||||
private string[] itemIdentifiers;
|
||||
public IEnumerable<string> Identifiers => itemIdentifiers;
|
||||
private string[] identifiersOrTags;
|
||||
|
||||
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
|
||||
private bool spawnItemIfNotFound = false;
|
||||
@@ -50,26 +48,26 @@ namespace Barotrauma
|
||||
moveToTarget = targetItem?.GetRootInventoryOwner();
|
||||
}
|
||||
|
||||
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
|
||||
: this(character, new string[] { itemIdentifier }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { }
|
||||
public AIObjectiveGetItem(Character character, string identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
|
||||
: this(character, new string[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { }
|
||||
|
||||
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
|
||||
public AIObjectiveGetItem(Character character, string[] identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
currSearchIndex = -1;
|
||||
this.equip = equip;
|
||||
this.itemIdentifiers = itemIdentifiers;
|
||||
this.identifiersOrTags = identifiersOrTags;
|
||||
this.spawnItemIfNotFound = spawnItemIfNotFound;
|
||||
for (int i = 0; i < itemIdentifiers.Length; i++)
|
||||
for (int i = 0; i < identifiersOrTags.Length; i++)
|
||||
{
|
||||
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
|
||||
identifiersOrTags[i] = identifiersOrTags[i].ToLowerInvariant();
|
||||
}
|
||||
this.checkInventory = checkInventory;
|
||||
}
|
||||
|
||||
private bool CheckInventory()
|
||||
{
|
||||
if (itemIdentifiers == null) { return false; }
|
||||
if (identifiersOrTags == null) { return false; }
|
||||
var item = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
|
||||
if (item != null)
|
||||
{
|
||||
@@ -86,7 +84,7 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (itemIdentifiers != null && !isDoneSeeking)
|
||||
if (identifiersOrTags != null && !isDoneSeeking)
|
||||
{
|
||||
if (checkInventory)
|
||||
{
|
||||
@@ -97,14 +95,18 @@ namespace Barotrauma
|
||||
}
|
||||
if (!isDoneSeeking)
|
||||
{
|
||||
bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0;
|
||||
if (dangerousPressure)
|
||||
if (!AllowDangerousPressure)
|
||||
{
|
||||
bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0 && character.PressureProtection <= 0;
|
||||
if (dangerousPressure)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Seeking item aborted, because the pressure is dangerous.", Color.Yellow);
|
||||
string itemName = targetItem != null ? targetItem.Name : identifiersOrTags.FirstOrDefault();
|
||||
DebugConsole.NewMessage($"{character.Name}: Seeking item ({itemName}) aborted, because the pressure is dangerous.", Color.Yellow);
|
||||
#endif
|
||||
Abandon = true;
|
||||
return;
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
FindTargetItem();
|
||||
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
|
||||
@@ -174,34 +176,20 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (equip)
|
||||
if (HumanAIController.TryToMoveItem(targetItem, character.Inventory))
|
||||
{
|
||||
if (HumanAIController.TryToMoveItem(targetItem, character.Inventory))
|
||||
if (equip)
|
||||
{
|
||||
targetItem.Equip(character);
|
||||
IsCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
|
||||
#endif
|
||||
Abandon = true;
|
||||
}
|
||||
IsCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (character.Inventory.TryPutItem(targetItem, character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Abandon = true;
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
|
||||
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
|
||||
#endif
|
||||
}
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
else if (moveToTarget != null)
|
||||
@@ -228,7 +216,7 @@ namespace Barotrauma
|
||||
|
||||
private void FindTargetItem()
|
||||
{
|
||||
if (itemIdentifiers == null)
|
||||
if (identifiersOrTags == null)
|
||||
{
|
||||
if (targetItem == null)
|
||||
{
|
||||
@@ -244,18 +232,16 @@ namespace Barotrauma
|
||||
currSearchIndex++;
|
||||
var item = Item.ItemList[currSearchIndex];
|
||||
Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine;
|
||||
Submarine mySub = character.Submarine;
|
||||
if (itemSub == null) { continue; }
|
||||
if (itemSub.TeamID != character.TeamID) { continue; }
|
||||
if (mySub == null) { continue; }
|
||||
if (itemSub.TeamID != mySub.TeamID && itemSub.TeamID != character.TeamID) { continue; }
|
||||
if (!CheckItem(item)) { continue; }
|
||||
if (ignoredContainerIdentifiers != null && item.Container != null)
|
||||
{
|
||||
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
|
||||
}
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
if (itemSub.Info.Type != character.Submarine.Info.Type) { continue; }
|
||||
if (character.Submarine.GetConnectedSubs().None(s => s == itemSub && itemSub.TeamID == character.TeamID && itemSub.Info.Type == character.Submarine.Info.Type)) { continue; }
|
||||
}
|
||||
if (!mySub.IsConnectedTo(itemSub)) { continue; }
|
||||
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
|
||||
float itemPriority = 1;
|
||||
if (GetItemPriority != null)
|
||||
@@ -283,10 +269,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (spawnItemIfNotFound)
|
||||
{
|
||||
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && itemIdentifiers.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
|
||||
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && identifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
|
||||
#endif
|
||||
Abandon = true;
|
||||
}
|
||||
@@ -305,7 +291,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow);
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
|
||||
#endif
|
||||
Abandon = true;
|
||||
}
|
||||
@@ -320,7 +306,7 @@ namespace Barotrauma
|
||||
{
|
||||
return character.HasItem(targetItem, equip);
|
||||
}
|
||||
else if (itemIdentifiers != null)
|
||||
else if (identifiersOrTags != null)
|
||||
{
|
||||
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
|
||||
if (matchingItem != null)
|
||||
@@ -338,13 +324,13 @@ namespace Barotrauma
|
||||
if (ignoredItems.Contains(item)) { return false; };
|
||||
if (item.Condition < TargetCondition) { return false; }
|
||||
if (ItemFilter != null && !ItemFilter(item)) { return false; }
|
||||
return itemIdentifiers.Any(id => id == item.Prefab.Identifier || item.HasTag(id));
|
||||
return identifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id));
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
goToObjective = null;
|
||||
targetItem = originalTarget;
|
||||
moveToTarget = targetItem?.GetRootInventoryOwner();
|
||||
isDoneSeeking = false;
|
||||
|
||||
+30
-12
@@ -1,7 +1,6 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -31,7 +30,7 @@ namespace Barotrauma
|
||||
public bool mimic;
|
||||
|
||||
private float _closeEnough = 50;
|
||||
private readonly float minDistance = 25;
|
||||
private readonly float minDistance = 50;
|
||||
/// <summary>
|
||||
/// Display units
|
||||
/// </summary>
|
||||
@@ -43,6 +42,8 @@ namespace Barotrauma
|
||||
_closeEnough = Math.Max(minDistance, value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool CheckVisibility { get; set; }
|
||||
public bool IgnoreIfTargetDead { get; set; }
|
||||
public bool AllowGoingOutside { get; set; }
|
||||
|
||||
@@ -103,7 +104,7 @@ namespace Barotrauma
|
||||
else if (Target is Character)
|
||||
{
|
||||
//if closeEnough value is given, allow setting CloseEnough as low as 50, otherwise above AIObjectiveGetItem.DefaultReach
|
||||
CloseEnough = Math.Max(closeEnough, MathUtils.NearlyEqual(closeEnough, 0.0f) ? AIObjectiveGetItem.DefaultReach : 50);
|
||||
CloseEnough = Math.Max(closeEnough, MathUtils.NearlyEqual(closeEnough, 0.0f) ? AIObjectiveGetItem.DefaultReach : minDistance);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -138,7 +139,7 @@ namespace Barotrauma
|
||||
}
|
||||
Target = Character.Controlled;
|
||||
}
|
||||
if (Target == character)
|
||||
if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
|
||||
{
|
||||
// Wait
|
||||
character.AIController.SteeringManager.Reset();
|
||||
@@ -207,7 +208,7 @@ namespace Barotrauma
|
||||
{
|
||||
Character followTarget = Target as Character;
|
||||
bool needsDivingSuit = targetIsOutside;
|
||||
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(character, targetHull, out needsDivingSuit);
|
||||
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
|
||||
if (!needsDivingGear && mimic)
|
||||
{
|
||||
if (HumanAIController.HasDivingSuit(followTarget))
|
||||
@@ -223,17 +224,25 @@ namespace Barotrauma
|
||||
bool needsEquipment = false;
|
||||
if (needsDivingSuit)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
|
||||
}
|
||||
else if (needsDivingGear)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
|
||||
}
|
||||
if (needsEquipment)
|
||||
{
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref findDivingGear));
|
||||
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
|
||||
{
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref findDivingGear));
|
||||
}
|
||||
else
|
||||
{
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
|
||||
onCompleted: () => RemoveSubObjective(ref findDivingGear));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -262,11 +271,14 @@ namespace Barotrauma
|
||||
{
|
||||
if (n.Waypoint.isObstructed) { return false; }
|
||||
return (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null);
|
||||
}, endNodeFilter, nodeFilter);
|
||||
}, endNodeFilter, nodeFilter, CheckVisibility);
|
||||
if (!isInside && PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -392,5 +404,11 @@ namespace Barotrauma
|
||||
HumanAIController.FaceTarget(Target);
|
||||
base.OnCompleted();
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
findDivingGear = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+83
-24
@@ -10,7 +10,7 @@ namespace Barotrauma
|
||||
class AIObjectiveIdle : AIObjective
|
||||
{
|
||||
public override string DebugTag => "idle";
|
||||
public override bool UnequipItems => true;
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
|
||||
private BehaviorType behavior;
|
||||
@@ -69,6 +69,8 @@ namespace Barotrauma
|
||||
const float chairCheckInterval = 5.0f;
|
||||
private float chairCheckTimer;
|
||||
|
||||
private float autonomousObjectiveRetryTimer = 10;
|
||||
|
||||
private readonly List<Hull> targetHulls = new List<Hull>(20);
|
||||
private readonly List<float> hullWeights = new List<float>(20);
|
||||
|
||||
@@ -88,11 +90,6 @@ namespace Barotrauma
|
||||
|
||||
public readonly HashSet<string> PreferredOutpostModuleTypes = new HashSet<string>();
|
||||
|
||||
private bool IsInWrongSub() =>
|
||||
character.Submarine == null ||
|
||||
currentTarget != null && currentTarget.Submarine != character.Submarine ||
|
||||
character.TeamID == Character.TeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID;
|
||||
|
||||
public void CalculatePriority(float max = 0)
|
||||
{
|
||||
//Random = Rand.Range(0.5f, 1.5f);
|
||||
@@ -149,6 +146,18 @@ namespace Barotrauma
|
||||
{
|
||||
if (PathSteering == null) { return; }
|
||||
|
||||
if (objectiveManager.FailedAutonomousObjectives)
|
||||
{
|
||||
if (autonomousObjectiveRetryTimer > 0)
|
||||
{
|
||||
autonomousObjectiveRetryTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
objectiveManager.CreateAutonomousObjectives();
|
||||
}
|
||||
}
|
||||
|
||||
//don't keep dragging others when idling
|
||||
if (character.SelectedCharacter != null)
|
||||
{
|
||||
@@ -160,13 +169,34 @@ namespace Barotrauma
|
||||
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
|
||||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
|
||||
|
||||
bool IsSteeringFinished() => PathSteering.CurrentPath != null && PathSteering.CurrentPath.Finished;
|
||||
bool IsSteeringFinished() => PathSteering.CurrentPath != null && (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable);
|
||||
|
||||
if (currentTargetIsInvalid || currentTarget == null || IsSteeringFinished() && (IsForbidden(character.CurrentHull) || IsInWrongSub()))
|
||||
if (currentTarget != null && !currentTargetIsInvalid)
|
||||
{
|
||||
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
|
||||
//almost constantly when there's a small number of potential hulls to move to
|
||||
SetTargetTimerLow();
|
||||
if (character.TeamID == Character.TeamType.FriendlyNPC)
|
||||
{
|
||||
if (currentTarget.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentTarget.Submarine != character.Submarine)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
|
||||
{
|
||||
if (newTargetTimer > timerMargin)
|
||||
{
|
||||
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
|
||||
//almost constantly when there's a small number of potential hulls to move to
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
else if (character.IsClimbing)
|
||||
{
|
||||
@@ -200,7 +230,8 @@ namespace Barotrauma
|
||||
{
|
||||
//choose a random available hull
|
||||
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
bool isCurrentHullAllowed = !IsInWrongSub() && !IsForbidden(character.CurrentHull);
|
||||
bool isInWrongSub = character.TeamID == Character.TeamType.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 =>
|
||||
{
|
||||
if (node.Waypoint.CurrentHull == null) { return false; }
|
||||
@@ -285,12 +316,12 @@ namespace Barotrauma
|
||||
if (standStillTimer > 0.0f)
|
||||
{
|
||||
walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
|
||||
|
||||
if (character.CurrentHull != null && character.CurrentHull.Rect.Width > 150 && tooCloseCharacter == null)
|
||||
var currentHull = character.CurrentHull;
|
||||
if (currentHull != null && currentHull.Rect.Width > IndoorsSteeringManager.smallRoomSize / 2 && tooCloseCharacter == null)
|
||||
{
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c == character || !c.IsBot || c.CurrentHull != character.CurrentHull || !(c.AIController is HumanAIController humanAI)) { continue; }
|
||||
if (c == character || !c.IsBot || c.CurrentHull != currentHull || !(c.AIController is HumanAIController humanAI)) { continue; }
|
||||
if (Vector2.DistanceSquared(c.WorldPosition, character.WorldPosition) > 60.0f * 60.0f) { continue; }
|
||||
if ((humanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective && idleObjective.standStillTimer > 0.0f) ||
|
||||
(humanAI.ObjectiveManager.CurrentObjective is AIObjectiveGoTo gotoObjective && gotoObjective.IsCloseEnough))
|
||||
@@ -303,7 +334,7 @@ namespace Barotrauma
|
||||
tooCloseCharacter = null;
|
||||
break;
|
||||
}
|
||||
tooCloseCharacter = c;
|
||||
tooCloseCharacter = c;
|
||||
}
|
||||
HumanAIController.FaceTarget(c);
|
||||
}
|
||||
@@ -313,9 +344,24 @@ namespace Barotrauma
|
||||
{
|
||||
Vector2 diff = character.WorldPosition - tooCloseCharacter.WorldPosition;
|
||||
if (diff.LengthSquared() < 0.0001f) { diff = Rand.Vector(1.0f); }
|
||||
if (diff.X > 0 && character.WorldPosition.X > character.CurrentHull.WorldRect.Right - 50) { diff.X = -diff.X; }
|
||||
if (diff.X < 0 && character.WorldPosition.X < character.CurrentHull.WorldRect.X + 50) { diff.X = -diff.X; }
|
||||
PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
|
||||
if (Math.Abs(diff.X) > 0 &&
|
||||
(character.WorldPosition.X > currentHull.WorldRect.Right - 50 || character.WorldPosition.X < currentHull.WorldRect.Left + 50))
|
||||
{
|
||||
// Between a wall and a character -> move away
|
||||
tooCloseCharacter = null;
|
||||
PathSteering.Reset();
|
||||
standStillTimer = 0;
|
||||
walkDuration = Math.Min(walkDuration, walkDurationMin);
|
||||
if (Behavior != BehaviorType.StayInHull && (currentHull.Size.X < IndoorsSteeringManager.smallRoomSize || currentHull.Size.X < (IndoorsSteeringManager.smallRoomSize / 2 * Character.CharacterList.Count(c => c.CurrentHull == currentHull))))
|
||||
{
|
||||
// Small room -> find another
|
||||
newTargetTimer = Math.Min(newTargetTimer, 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -329,14 +375,13 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.CurrentHull != character.CurrentHull || !item.HasTag("chair")) { continue; }
|
||||
if (item.CurrentHull != currentHull || !item.HasTag("chair")) { continue; }
|
||||
var controller = item.GetComponent<Controller>();
|
||||
if (controller == null || controller.User != null) { continue; }
|
||||
item.TryInteract(character, forceSelectKey: true);
|
||||
}
|
||||
chairCheckTimer = chairCheckInterval;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (standStillTimer < -walkDuration)
|
||||
@@ -376,7 +421,9 @@ namespace Barotrauma
|
||||
}
|
||||
if (IsForbidden(hull)) { continue; }
|
||||
// Check that the hull is linked
|
||||
if (!character.Submarine.GetConnectedSubs().Contains(hull.Submarine)) { continue; }
|
||||
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
|
||||
// Ignore very narrow hulls.
|
||||
if (hull.RectWidth < 200) { continue; }
|
||||
// Ignore hulls that are too low to stand inside.
|
||||
if (character.AnimController is HumanoidAnimController animController)
|
||||
{
|
||||
@@ -388,13 +435,14 @@ namespace Barotrauma
|
||||
if (!targetHulls.Contains(hull))
|
||||
{
|
||||
targetHulls.Add(hull);
|
||||
float weight = hull.Volume;
|
||||
float weight = hull.RectWidth;
|
||||
// Prefer rooms that are closer. Avoid rooms that are not in the same level.
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
|
||||
yDist = yDist > 100 ? yDist * 5 : 0;
|
||||
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
|
||||
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist));
|
||||
weight *= distanceFactor;
|
||||
float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2));
|
||||
weight *= distanceFactor * waterFactor;
|
||||
hullWeights.Add(weight);
|
||||
}
|
||||
}
|
||||
@@ -418,5 +466,16 @@ namespace Barotrauma
|
||||
if (hullName == null) { return false; }
|
||||
return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
currentTarget = null;
|
||||
searchingNewHull = false;
|
||||
tooCloseCharacter = null;
|
||||
targetHulls.Clear();
|
||||
hullWeights.Clear();
|
||||
autonomousObjectiveRetryTimer = 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-6
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
@@ -89,10 +88,6 @@ namespace Barotrauma
|
||||
{
|
||||
syncTimer -= deltaTime;
|
||||
}
|
||||
if (Objectives.None() && Targets.Any(t => !ignoreList.Contains(t)))
|
||||
{
|
||||
CreateObjectives();
|
||||
}
|
||||
}
|
||||
|
||||
// the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
|
||||
@@ -113,7 +108,7 @@ namespace Barotrauma
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
if (character.LockHands || character.Submarine == null || Targets.None())
|
||||
if (character.LockHands || character.Submarine == null)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
|
||||
+7
-2
@@ -92,6 +92,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
|
||||
public bool FailedAutonomousObjectives { get; private set; }
|
||||
|
||||
private void ClearIgnored()
|
||||
{
|
||||
@@ -118,10 +119,11 @@ namespace Barotrauma
|
||||
}
|
||||
DelayedObjectives.Clear();
|
||||
Objectives.Clear();
|
||||
FailedAutonomousObjectives = false;
|
||||
AddObjective(new AIObjectiveFindSafety(character, this));
|
||||
AddObjective(new AIObjectiveIdle(character, this));
|
||||
int objectiveCount = Objectives.Count;
|
||||
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjective)
|
||||
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
|
||||
{
|
||||
var orderPrefab = Order.GetPrefab(autonomousObjective.identifier);
|
||||
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); }
|
||||
@@ -129,6 +131,7 @@ namespace Barotrauma
|
||||
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity,
|
||||
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character);
|
||||
if (order == null) { continue; }
|
||||
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != Character.TeamType.FriendlyNPC) { continue; }
|
||||
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
|
||||
if (objective != null && objective.CanBeCompleted)
|
||||
{
|
||||
@@ -220,7 +223,7 @@ namespace Barotrauma
|
||||
if (objective.IsCompleted)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightGreen);
|
||||
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightBlue);
|
||||
#endif
|
||||
Objectives.Remove(objective);
|
||||
}
|
||||
@@ -230,6 +233,7 @@ namespace Barotrauma
|
||||
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it cannot be completed.", Color.Red);
|
||||
#endif
|
||||
Objectives.Remove(objective);
|
||||
FailedAutonomousObjectives = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -286,6 +290,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// This should be redundant, because all the objectives are reset when they are selected as active.
|
||||
CurrentOrder.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
+42
-7
@@ -8,8 +8,9 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveOperateItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "operate item";
|
||||
public override bool UnequipItems => true;
|
||||
public override string DebugTag => $"operate item {component.Name}";
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowMultipleInstances => true;
|
||||
|
||||
private ItemComponent component, controller;
|
||||
private Entity operateTarget;
|
||||
@@ -22,6 +23,8 @@ namespace Barotrauma
|
||||
|
||||
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
|
||||
|
||||
public override bool IsDuplicate<T>(T otherObjective) => base.IsDuplicate(otherObjective) && otherObjective is AIObjectiveOperateItem operateObjective && operateObjective.component == component;
|
||||
|
||||
public Entity OperateTarget => operateTarget;
|
||||
public ItemComponent Component => component;
|
||||
|
||||
@@ -32,7 +35,7 @@ namespace Barotrauma
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
if (!IsAllowed || character.LockHands)
|
||||
{
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
@@ -43,7 +46,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
bool isOrder = objectiveManager.CurrentOrder == this;
|
||||
if (isOrder)
|
||||
{
|
||||
Priority = AIObjectiveManager.OrderPriority;
|
||||
}
|
||||
@@ -61,6 +65,16 @@ namespace Barotrauma
|
||||
var reactor = component?.Item.GetComponent<Reactor>();
|
||||
if (reactor != null)
|
||||
{
|
||||
if (!isOrder)
|
||||
{
|
||||
if (reactor.LastUserWasPlayer && character.TeamID != Character.TeamType.FriendlyNPC ||
|
||||
HumanAIController.IsTrueForAnyCrewMember(c =>
|
||||
c.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() == target))
|
||||
{
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
}
|
||||
switch (Option)
|
||||
{
|
||||
case "shutdown":
|
||||
@@ -73,7 +87,7 @@ namespace Barotrauma
|
||||
case "powerup":
|
||||
// Check that we don't already have another order that is targeting the same item.
|
||||
// Without this the autonomous objective will tell the bot to turn the reactor on again.
|
||||
if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target)
|
||||
if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target && operateOrder.Option != Option)
|
||||
{
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
@@ -82,7 +96,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
if (targetItem.CurrentHull == null ||
|
||||
targetItem.Submarine != character.Submarine && objectiveManager.CurrentOrder != this ||
|
||||
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)))
|
||||
@@ -92,7 +106,12 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
|
||||
float max = objectiveManager.CurrentOrder == this ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1;
|
||||
float max = isOrder ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1;
|
||||
if (!isOrder && reactor != null && reactor.PowerOn && Option == "powerup")
|
||||
{
|
||||
// Decrease the priority when targeting a reactor that is already on.
|
||||
value /= 2;
|
||||
}
|
||||
Priority = MathHelper.Clamp(value, 0, max);
|
||||
}
|
||||
}
|
||||
@@ -142,6 +161,15 @@ namespace Barotrauma
|
||||
// Don't abandon
|
||||
return;
|
||||
}
|
||||
if (operateTarget != null)
|
||||
{
|
||||
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
|
||||
{
|
||||
// Another crew member is already targeting this entity.
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (target.CanBeSelected)
|
||||
{
|
||||
if (character.CanInteractWith(target.Item, out _, checkLinked: false))
|
||||
@@ -227,5 +255,12 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
protected override bool Check() => isDoneOperating && !IsLoop;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
goToObjective = null;
|
||||
getItemObjective = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -10,7 +11,7 @@ namespace Barotrauma
|
||||
{
|
||||
public override string DebugTag => "pump water";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool UnequipItems => true;
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
|
||||
private IEnumerable<Pump> pumpList;
|
||||
|
||||
@@ -35,8 +36,7 @@ namespace Barotrauma
|
||||
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
if (pump.Item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; }
|
||||
if (!character.Submarine.IsConnectedTo(pump.Item.Submarine)) { return false; }
|
||||
}
|
||||
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
|
||||
if (IsReady(pump)) { return false; }
|
||||
@@ -54,6 +54,7 @@ namespace Barotrauma
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
if (Targets.None()) { return 0; }
|
||||
if (Option == "stoppumping")
|
||||
{
|
||||
return Targets.Max(t => MathHelper.Lerp(0, 100, Math.Abs(t.FlowPercentage / 100)));
|
||||
|
||||
+27
-14
@@ -9,7 +9,6 @@ namespace Barotrauma
|
||||
class AIObjectiveRepairItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "repair item";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
|
||||
public Item Item { get; private set; }
|
||||
|
||||
@@ -18,9 +17,11 @@ namespace Barotrauma
|
||||
private float previousCondition = -1;
|
||||
private RepairTool repairTool;
|
||||
|
||||
private bool IsRepairing => character.SelectedConstruction == Item && Item.GetComponent<Repairable>()?.CurrentFixer == character;
|
||||
private bool IsRepairing() => IsRepairing(character, Item);
|
||||
private readonly bool isPriority;
|
||||
|
||||
public static bool IsRepairing(Character character, Item item) => character.SelectedConstruction == item && item.Repairables.Any(r => r.CurrentFixer == character);
|
||||
|
||||
public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
@@ -49,12 +50,15 @@ namespace Barotrauma
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
|
||||
yDist = yDist > 100 ? yDist * 5 : 0;
|
||||
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
|
||||
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
|
||||
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist));
|
||||
}
|
||||
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor: objectiveManager.CurrentOrder != this ? AIObjectiveRepairItems.RequiredSuccessFactor : 0);
|
||||
float isSelected = IsRepairing ? 50 : 0;
|
||||
float devotion = (CumulatedDevotion + isSelected) / 100;
|
||||
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
|
||||
float requiredSuccessFactor = objectiveManager.IsCurrentOrder<AIObjectiveRepairItems>() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor;
|
||||
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100;
|
||||
bool isSelected = IsRepairing();
|
||||
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
|
||||
float devotion = (CumulatedDevotion + selectedBonus) / 100;
|
||||
float reduction = isPriority ? 1 : isSelected ? 2 : 3;
|
||||
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
}
|
||||
return Priority;
|
||||
@@ -63,7 +67,7 @@ namespace Barotrauma
|
||||
protected override bool Check()
|
||||
{
|
||||
IsCompleted = Item.IsFullCondition;
|
||||
if (IsCompleted && IsRepairing)
|
||||
if (IsCompleted && IsRepairing())
|
||||
{
|
||||
character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
|
||||
}
|
||||
@@ -95,7 +99,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (repairTool != null)
|
||||
{
|
||||
var containedItems = repairTool.Item.ContainedItems;
|
||||
var containedItems = repairTool.Item.OwnInventory?.Items;
|
||||
if (containedItems == null)
|
||||
{
|
||||
#if DEBUG
|
||||
@@ -118,13 +122,13 @@ namespace Barotrauma
|
||||
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
|
||||
{
|
||||
item = requiredItem;
|
||||
fuel = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
|
||||
fuel = containedItems.FirstOrDefault(it => it != null && 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),
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
|
||||
onCompleted: () => RemoveSubObjective(ref refuelObjective),
|
||||
onAbandon: () => Abandon = true);
|
||||
return;
|
||||
@@ -142,7 +146,7 @@ namespace Barotrauma
|
||||
if (repairable.CurrentFixer != null && repairable.CurrentFixer != character)
|
||||
{
|
||||
// Someone else is repairing the target. Abandon the objective if the other is better at this than us.
|
||||
Abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
|
||||
Abandon = repairable.CurrentFixer.IsPlayer || repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
|
||||
}
|
||||
if (!Abandon)
|
||||
{
|
||||
@@ -166,7 +170,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (Abandon)
|
||||
{
|
||||
if (IsRepairing)
|
||||
if (IsRepairing())
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
|
||||
}
|
||||
@@ -201,7 +205,7 @@ namespace Barotrauma
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
if (IsRepairing)
|
||||
if (IsRepairing())
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
|
||||
}
|
||||
@@ -251,5 +255,14 @@ namespace Barotrauma
|
||||
repairTool.Use(deltaTime, character);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
goToObjective = null;
|
||||
refuelObjective = null;
|
||||
previousCondition = -1;
|
||||
repairTool = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-5
@@ -90,18 +90,23 @@ namespace Barotrauma
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
if (character.SelectedConstruction != null && Targets.Any(t => character.SelectedConstruction == t && t.ConditionPercentage < 100))
|
||||
var selectedItem = character.SelectedConstruction;
|
||||
if (selectedItem != null && AIObjectiveRepairItem.IsRepairing(character, selectedItem) && selectedItem.ConditionPercentage < 100)
|
||||
{
|
||||
// Don't stop fixing until done
|
||||
// Don't stop fixing until completely done
|
||||
return 100;
|
||||
}
|
||||
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>() && !c.Character.IsIncapacitated, onlyBots: true);
|
||||
int items = Targets.Count;
|
||||
if (items == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
bool anyFixers = otherFixers > 0;
|
||||
float ratio = anyFixers ? items / (float)otherFixers : 1;
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
{
|
||||
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
|
||||
return Targets.Sum(t => 100 - t.ConditionPercentage);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -151,8 +156,7 @@ namespace Barotrauma
|
||||
if (item.Repairables.None()) { return false; }
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
|
||||
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+123
-44
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -21,10 +22,12 @@ namespace Barotrauma
|
||||
private readonly Character targetCharacter;
|
||||
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private AIObjectiveContainItem replaceOxygenObjective;
|
||||
private AIObjectiveGetItem getItemObjective;
|
||||
private float treatmentTimer;
|
||||
private Hull safeHull;
|
||||
private float findHullTimer;
|
||||
private bool ignoreOxygen;
|
||||
private readonly float findHullInterval = 1.0f;
|
||||
|
||||
public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
@@ -69,65 +72,130 @@ namespace Barotrauma
|
||||
}
|
||||
if (targetCharacter != character)
|
||||
{
|
||||
// Incapacitated target is not in a safe place -> Move to a safe place first
|
||||
if (targetCharacter.IsIncapacitated && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
if (targetCharacter.IsIncapacitated)
|
||||
{
|
||||
if (character.SelectedCharacter != targetCharacter)
|
||||
// Check if the character needs more oxygen
|
||||
if (!ignoreOxygen && character.SelectedCharacter == targetCharacter || character.CanInteractWith(targetCharacter))
|
||||
{
|
||||
if (targetCharacter.CurrentHull.DisplayName != null)
|
||||
// Replace empty oxygen tank
|
||||
// First remove empty tanks
|
||||
if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out IEnumerable<Item> suits, requireEquipped: true))
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
|
||||
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
|
||||
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
|
||||
}
|
||||
|
||||
// Go to the target and select it
|
||||
if (!character.CanInteractWith(targetCharacter))
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
|
||||
Item suit = suits.FirstOrDefault();
|
||||
if (suit != null)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient",
|
||||
TargetName = targetCharacter.DisplayName
|
||||
},
|
||||
AIObjectiveFindDivingGear.DropEmptyTanks(character, suit, out _);
|
||||
}
|
||||
}
|
||||
else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable<Item> masks, requireEquipped: true))
|
||||
{
|
||||
Item mask = masks.FirstOrDefault();
|
||||
if (mask != null)
|
||||
{
|
||||
AIObjectiveFindDivingGear.DropEmptyTanks(character, mask, out _);
|
||||
}
|
||||
}
|
||||
bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0;
|
||||
if (ShouldRemoveDivingSuit())
|
||||
{
|
||||
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)))
|
||||
{
|
||||
// The target has a suit equipped with an empty oxygen tank.
|
||||
// Can't remove the suit, because the target needs it.
|
||||
// If we happen to have an extra oxygen tank in the inventory, let's swap it.
|
||||
Item spareOxygenTank = FindOxygenTank(targetCharacter) ?? FindOxygenTank(character);
|
||||
if (spareOxygenTank != null)
|
||||
{
|
||||
Item suit = suits.FirstOrDefault();
|
||||
if (suit != null)
|
||||
{
|
||||
// Insert the new oxygen tank
|
||||
TryAddSubObjective(ref replaceOxygenObjective, () => new AIObjectiveContainItem(character, spareOxygenTank, suit.GetComponent<ItemContainer>(), objectiveManager),
|
||||
onCompleted: () => RemoveSubObjective(ref replaceOxygenObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref replaceOxygenObjective);
|
||||
ignoreOxygen = true;
|
||||
if (ShouldRemoveDivingSuit())
|
||||
{
|
||||
suits.ForEach(suit => suit.Drop(character));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Item FindOxygenTank(Character c) =>
|
||||
c.Inventory.FindItem(i =>
|
||||
i.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) &&
|
||||
i.ConditionPercentage > 1 &&
|
||||
i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag("diving")) == null,
|
||||
recursive: true);
|
||||
}
|
||||
}
|
||||
if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
// Incapacitated target is not in a safe place -> Move to a safe place first
|
||||
if (character.SelectedCharacter != targetCharacter)
|
||||
{
|
||||
if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
|
||||
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
|
||||
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
|
||||
}
|
||||
// Go to the target and select it
|
||||
if (!character.CanInteractWith(targetCharacter))
|
||||
{
|
||||
RemoveSubObjective(ref replaceOxygenObjective);
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient",
|
||||
TargetName = targetCharacter.DisplayName
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
Abandon = true;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
character.SelectCharacter(targetCharacter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Drag the character into safety
|
||||
if (safeHull == null)
|
||||
{
|
||||
if (findHullTimer > 0)
|
||||
{
|
||||
findHullTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
|
||||
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
|
||||
character.SelectCharacter(targetCharacter);
|
||||
}
|
||||
}
|
||||
if (safeHull != null && character.CurrentHull != safeHull)
|
||||
else
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
// Drag the character into safety
|
||||
if (safeHull == null)
|
||||
{
|
||||
if (findHullTimer > 0)
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
safeHull = character.CurrentHull;
|
||||
});
|
||||
findHullTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
|
||||
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
|
||||
}
|
||||
}
|
||||
if (safeHull != null && character.CurrentHull != safeHull)
|
||||
{
|
||||
RemoveSubObjective(ref replaceOxygenObjective);
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
safeHull = character.CurrentHull;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,6 +205,7 @@ namespace Barotrauma
|
||||
|
||||
if (targetCharacter != character && !character.CanInteractWith(targetCharacter))
|
||||
{
|
||||
RemoveSubObjective(ref replaceOxygenObjective);
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
// Go to the target and select it
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
|
||||
@@ -325,7 +394,7 @@ namespace Barotrauma
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
|
||||
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
@@ -346,5 +415,15 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
goToObjective = null;
|
||||
getItemObjective = null;
|
||||
replaceOxygenObjective = null;
|
||||
safeHull = null;
|
||||
ignoreOxygen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -34,6 +35,7 @@ namespace Barotrauma
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
if (Targets.None()) { return 100; }
|
||||
if (objectiveManager.CurrentOrder != this)
|
||||
{
|
||||
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious))
|
||||
@@ -72,7 +74,7 @@ namespace Barotrauma
|
||||
public static bool IsValidTarget(Character target, Character character)
|
||||
{
|
||||
if (target == null || target.IsDead || target.Removed) { return false; }
|
||||
if (target.TurnedHostileByEvent) { return false; }
|
||||
if (target.IsInstigator) { return false; }
|
||||
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
|
||||
if (character.AIController is HumanAIController humanAI)
|
||||
{
|
||||
@@ -83,7 +85,7 @@ namespace Barotrauma
|
||||
{
|
||||
// Don't allow to treat others autonomously
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Ignore unsafe hulls, unless ordered
|
||||
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
|
||||
{
|
||||
@@ -100,10 +102,11 @@ namespace Barotrauma
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; }
|
||||
if (target != character &&!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
|
||||
{
|
||||
// Ignore all concious targets that are currently fighting, fleeing or treating characters
|
||||
// Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters
|
||||
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
|
||||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
|
||||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
|
||||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
|
||||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFixLeak>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ namespace Barotrauma
|
||||
Order = orderInfo.Order;
|
||||
OrderOption = orderInfo.OrderOption;
|
||||
}
|
||||
|
||||
public bool MatchesOrder(Order order, string option) =>
|
||||
order.Identifier == Order.Identifier && option == OrderOption && order.TargetEntity == Order.TargetEntity;
|
||||
}
|
||||
|
||||
class Order
|
||||
@@ -100,8 +103,7 @@ namespace Barotrauma
|
||||
|
||||
public Character OrderGiver;
|
||||
|
||||
private readonly OrderCategory? category;
|
||||
public OrderCategory? Category => category;
|
||||
public OrderCategory? Category { get; private set; }
|
||||
|
||||
//legacy support
|
||||
public readonly string[] AppropriateJobs;
|
||||
@@ -225,7 +227,7 @@ namespace Barotrauma
|
||||
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
|
||||
Options = orderElement.GetAttributeStringArray("options", new string[0]);
|
||||
var category = orderElement.GetAttributeString("category", null);
|
||||
if (!string.IsNullOrWhiteSpace(category)) { this.category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
|
||||
if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
|
||||
Weight = orderElement.GetAttributeFloat(0.0f, "weight");
|
||||
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
|
||||
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
|
||||
@@ -299,7 +301,7 @@ namespace Barotrauma
|
||||
Weight = prefab.Weight;
|
||||
MustSetTarget = prefab.MustSetTarget;
|
||||
AppropriateSkill = prefab.AppropriateSkill;
|
||||
category = prefab.Category;
|
||||
Category = prefab.Category;
|
||||
MustManuallyAssign = prefab.MustManuallyAssign;
|
||||
|
||||
OrderGiver = orderGiver;
|
||||
|
||||
@@ -160,7 +160,7 @@ namespace Barotrauma
|
||||
|
||||
private static readonly List<PathNode> sortedNodes = new List<PathNode>();
|
||||
|
||||
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
|
||||
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
|
||||
{
|
||||
//sort nodes roughly according to distance
|
||||
sortedNodes.Clear();
|
||||
@@ -202,12 +202,12 @@ namespace Barotrauma
|
||||
//if searching for a path inside the sub, make sure the waypoint is visible
|
||||
if (InsideSubmarine)
|
||||
{
|
||||
// Always check the visibility for the start node
|
||||
var body = Submarine.PickBody(
|
||||
start, node.TempPosition, null,
|
||||
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
|
||||
if (body != null)
|
||||
{
|
||||
//if (body.UserData is Submarine) continue;
|
||||
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
|
||||
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
|
||||
}
|
||||
@@ -257,14 +257,13 @@ namespace Barotrauma
|
||||
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
|
||||
|
||||
//if searching for a path inside the sub, make sure the waypoint is visible
|
||||
if (InsideSubmarine)
|
||||
if (InsideSubmarine && checkVisibility)
|
||||
{
|
||||
// Only check the visibility for the end node when allowed (fix leaks)
|
||||
var body = Submarine.PickBody(end, node.TempPosition, null,
|
||||
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs );
|
||||
|
||||
if (body != null)
|
||||
{
|
||||
//if (body.UserData is Submarine) continue;
|
||||
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
|
||||
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
|
||||
}
|
||||
|
||||
@@ -78,11 +78,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (!aiController.Enabled) { return; }
|
||||
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
|
||||
if (Controlled == this) { return; }
|
||||
|
||||
if (!IsRemotelyControlled)
|
||||
|
||||
if (!IsRemotelyControlled && aiController != null && aiController.Enabled)
|
||||
{
|
||||
aiController.Update(deltaTime);
|
||||
}
|
||||
|
||||
+13
-3
@@ -613,8 +613,13 @@ namespace Barotrauma
|
||||
movementAngle -= MathHelper.TwoPi;
|
||||
}
|
||||
|
||||
float offset = MathHelper.Pi * CurrentGroundedParams.StepLiftOffset;
|
||||
if (CurrentGroundedParams.MultiplyByDir)
|
||||
{
|
||||
offset *= Dir;
|
||||
}
|
||||
float stepLift = TargetMovement.X == 0.0f ? 0 :
|
||||
(float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100);
|
||||
(float)Math.Sin(WalkPos * Dir * CurrentGroundedParams.StepLiftFrequency + offset) * (CurrentGroundedParams.StepLiftAmount / 100);
|
||||
|
||||
float limpAmount = character.GetLegPenalty();
|
||||
if (limpAmount > 0)
|
||||
@@ -631,7 +636,7 @@ namespace Barotrauma
|
||||
{
|
||||
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque);
|
||||
}
|
||||
if (TorsoPosition.HasValue)
|
||||
if (TorsoPosition.HasValue && TorsoMoveForce > 0.0f)
|
||||
{
|
||||
Vector2 pos = colliderBottom + new Vector2(limpAmount, TorsoPosition.Value + stepLift);
|
||||
|
||||
@@ -649,11 +654,16 @@ namespace Barotrauma
|
||||
Limb head = GetLimb(LimbType.Head);
|
||||
if (head != null)
|
||||
{
|
||||
bool headFacingBackwards = false;
|
||||
if (HeadAngle.HasValue)
|
||||
{
|
||||
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque);
|
||||
if (Math.Sign(head.SimPosition.X - mainLimb.SimPosition.X) != Math.Sign(Dir))
|
||||
{
|
||||
headFacingBackwards = true;
|
||||
}
|
||||
}
|
||||
if (HeadPosition.HasValue)
|
||||
if (HeadPosition.HasValue && HeadMoveForce > 0.0f && !headFacingBackwards)
|
||||
{
|
||||
Vector2 pos = colliderBottom + new Vector2(limpAmount, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier);
|
||||
|
||||
|
||||
+3
-5
@@ -1873,6 +1873,7 @@ namespace Barotrauma
|
||||
forearm = GetLimb(LimbType.RightForearm);
|
||||
RightHandIKPos = pos;
|
||||
}
|
||||
if (arm == null) { return; }
|
||||
|
||||
//distance from shoulder to holdpos
|
||||
float c = Vector2.Distance(pos, shoulderPos);
|
||||
@@ -2018,12 +2019,9 @@ namespace Barotrauma
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (character.SelectedItems[i]?.body != null && !character.SelectedItems[i].Removed)
|
||||
if (character.SelectedItems[i]?.body != null && !character.SelectedItems[i].Removed && character.SelectedItems[i].GetComponent<Holdable>() != null)
|
||||
{
|
||||
/*character.SelectedItems[i].body.SetTransform(
|
||||
character.SelectedItems[i].body.SimPosition,
|
||||
MathUtils.WrapAngleTwoPi(character.SelectedItems[i].body.Rotation + MathHelper.Pi));*/
|
||||
character.SelectedItems[i].GetComponent<Holdable>()?.Flip();
|
||||
character.SelectedItems[i].FlipX(relativeToSub: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -764,6 +764,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(character.BloodDecalName))
|
||||
{
|
||||
character.CurrentHull?.AddDecal(character.BloodDecalName,
|
||||
(limbJoint.LimbA.WorldPosition + limbJoint.LimbB.WorldPosition) / 2, MathHelper.Clamp(Math.Min(limbJoint.LimbA.Mass, limbJoint.LimbB.Mass), 0.5f, 2.0f), true);
|
||||
}
|
||||
|
||||
SeverLimbJointProjSpecific(limbJoint, playSound: true);
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
@@ -1430,6 +1437,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void ForceRefreshFloorY()
|
||||
{
|
||||
lastFloorCheckPos = Vector2.Zero;
|
||||
}
|
||||
|
||||
private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false)
|
||||
{
|
||||
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
|
||||
|
||||
@@ -184,7 +184,7 @@ namespace Barotrauma
|
||||
[Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable]
|
||||
public Vector2 TargetForceWorld { get; private set; }
|
||||
|
||||
[Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed when the target is dead."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
|
||||
[Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
|
||||
public float SeverLimbsProbability { get; set; }
|
||||
|
||||
// TODO: disabled because not synced
|
||||
@@ -235,7 +235,7 @@ namespace Barotrauma
|
||||
List<Affliction> multipliedAfflictions = new List<Affliction>();
|
||||
foreach (Affliction affliction in Afflictions.Keys)
|
||||
{
|
||||
multipliedAfflictions.Add(affliction.Prefab.Instantiate(affliction.Strength * multiplier, affliction.Source));
|
||||
multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier));
|
||||
}
|
||||
return multipliedAfflictions;
|
||||
}
|
||||
@@ -495,14 +495,14 @@ namespace Barotrauma
|
||||
if (SecondaryCoolDownTimer < 0) { SecondaryCoolDownTimer = 0; }
|
||||
}
|
||||
|
||||
public void UpdateAttackTimer(float deltaTime)
|
||||
public void UpdateAttackTimer(float deltaTime, Character character)
|
||||
{
|
||||
IsRunning = true;
|
||||
AttackTimer += deltaTime;
|
||||
if (AttackTimer >= Duration)
|
||||
{
|
||||
ResetAttackTimer();
|
||||
SetCoolDown();
|
||||
SetCoolDown(applyRandom: !character.IsPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,13 +512,22 @@ namespace Barotrauma
|
||||
IsRunning = false;
|
||||
}
|
||||
|
||||
public void SetCoolDown()
|
||||
public void SetCoolDown(bool applyRandom)
|
||||
{
|
||||
float randomFraction = CoolDown * CoolDownRandomFactor;
|
||||
CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
|
||||
CoolDownTimer = CoolDown + CurrentRandomCoolDown;
|
||||
randomFraction = SecondaryCoolDown * CoolDownRandomFactor;
|
||||
SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
|
||||
if (applyRandom)
|
||||
{
|
||||
float randomFraction = CoolDown * CoolDownRandomFactor;
|
||||
CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
|
||||
CoolDownTimer = CoolDown + CurrentRandomCoolDown;
|
||||
randomFraction = SecondaryCoolDown * CoolDownRandomFactor;
|
||||
SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
|
||||
}
|
||||
else
|
||||
{
|
||||
CoolDownTimer = CoolDown;
|
||||
SecondaryCoolDownTimer = SecondaryCoolDown;
|
||||
CurrentRandomCoolDown = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetCoolDown()
|
||||
|
||||
@@ -121,7 +121,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool TurnedHostileByEvent;
|
||||
public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator;
|
||||
public CombatAction CombatAction;
|
||||
|
||||
public AnimController AnimController;
|
||||
|
||||
@@ -143,6 +144,8 @@ namespace Barotrauma
|
||||
public bool IsHumanoid => Params.Humanoid;
|
||||
public bool IsHusk => Params.Husk;
|
||||
|
||||
public string BloodDecalName => Params.BloodDecal;
|
||||
|
||||
public bool CanSpeak
|
||||
{
|
||||
get => Params.CanSpeak;
|
||||
@@ -637,6 +640,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool GodMode = false;
|
||||
|
||||
public CampaignMode.InteractionType CampaignInteractionType;
|
||||
|
||||
private bool accessRemovedCharacterErrorShown;
|
||||
@@ -1668,14 +1673,14 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool HasEquippedItem(string itemIdentifier, bool allowBroken = true)
|
||||
public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true)
|
||||
{
|
||||
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 == itemIdentifier || Inventory.Items[i].HasTag(itemIdentifier)) { return true; }
|
||||
if (Inventory.Items[i].Prefab.Identifier == tagOrIdentifier || Inventory.Items[i].HasTag(tagOrIdentifier)) { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -1738,7 +1743,7 @@ namespace Barotrauma
|
||||
if (inventory.Owner is Item)
|
||||
{
|
||||
var owner = (Item)inventory.Owner;
|
||||
if (!CanInteractWith(owner)) { return false; }
|
||||
if (!CanInteractWith(owner) && !owner.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; }
|
||||
ItemContainer container = owner.GetComponents<ItemContainer>().FirstOrDefault(ic => ic.Inventory == inventory);
|
||||
if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; }
|
||||
}
|
||||
@@ -1833,7 +1838,7 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
|
||||
#endif
|
||||
if (!CanInteract || hidden || item.NonInteractable) return false;
|
||||
if (!CanInteract || hidden || item.NonInteractable) { return false; }
|
||||
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
@@ -1845,6 +1850,7 @@ namespace Barotrauma
|
||||
{
|
||||
//locked wires are never interactable
|
||||
if (wire.Locked) { return false; }
|
||||
if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { return false; }
|
||||
|
||||
//wires are interactable if the character has selected an item the wire is connected to,
|
||||
//and it's disconnected from the other end
|
||||
@@ -2314,7 +2320,7 @@ namespace Barotrauma
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
Implode();
|
||||
return;
|
||||
if (IsDead) { return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2329,7 +2335,7 @@ namespace Barotrauma
|
||||
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
|
||||
{
|
||||
Implode();
|
||||
return;
|
||||
if (IsDead) { return; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2509,6 +2515,12 @@ namespace Barotrauma
|
||||
if (subCorpseCount < GameMain.Config.CorpsesPerSubDespawnThreshold) { return; }
|
||||
}
|
||||
|
||||
if (SelectedBy != null)
|
||||
{
|
||||
despawnTimer = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
float distToClosestPlayer = GetDistanceToClosestPlayer();
|
||||
if (distToClosestPlayer > NetConfig.DisableCharacterDist)
|
||||
{
|
||||
@@ -2866,20 +2878,18 @@ namespace Barotrauma
|
||||
wasSevered = severed;
|
||||
}
|
||||
if (severed)
|
||||
{
|
||||
{
|
||||
Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA;
|
||||
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass);
|
||||
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass);
|
||||
ApplyStatusEffects(ActionType.OnSevered, 1.0f);
|
||||
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
|
||||
otherLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
|
||||
}
|
||||
}
|
||||
if (wasSevered)
|
||||
if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI)
|
||||
{
|
||||
if (targetLimb.character.AIController is EnemyAIController enemyAI)
|
||||
{
|
||||
enemyAI.ReevaluateAttacks();
|
||||
}
|
||||
ApplyStatusEffects(ActionType.OnSevered, 1.0f);
|
||||
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
|
||||
}
|
||||
enemyAI.ReevaluateAttacks();
|
||||
}
|
||||
}
|
||||
|
||||
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null)
|
||||
@@ -3075,7 +3085,7 @@ namespace Barotrauma
|
||||
|
||||
private void Implode(bool isNetworkMessage = false)
|
||||
{
|
||||
if (CharacterHealth.Unkillable || IsDead) { return; }
|
||||
if (CharacterHealth.Unkillable || GodMode || IsDead) { return; }
|
||||
|
||||
if (!isNetworkMessage)
|
||||
{
|
||||
@@ -3127,7 +3137,7 @@ namespace Barotrauma
|
||||
|
||||
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true)
|
||||
{
|
||||
if (IsDead || CharacterHealth.Unkillable) { return; }
|
||||
if (IsDead || CharacterHealth.Unkillable || GodMode) { return; }
|
||||
|
||||
HealthUpdateInterval = 0.0f;
|
||||
|
||||
@@ -3447,7 +3457,7 @@ namespace Barotrauma
|
||||
public bool IsMechanic => HasJob("mechanic");
|
||||
public bool IsMedic => HasJob("medicaldoctor");
|
||||
public bool IsSecurity => HasJob("securityofficer");
|
||||
public bool IsAsssitant => HasJob("assistant");
|
||||
public bool IsAssistant => HasJob("assistant");
|
||||
public bool IsWatchman => HasJob("watchman");
|
||||
|
||||
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace Barotrauma
|
||||
head = value;
|
||||
if (head.race == Race.None)
|
||||
{
|
||||
head.race = GetRandomRace();
|
||||
head.race = GetRandomRace(Rand.RandSync.Unsynced);
|
||||
}
|
||||
CalculateHeadSpriteRange();
|
||||
Head.HeadSpriteId = value.HeadSpriteId;
|
||||
@@ -296,7 +296,7 @@ namespace Barotrauma
|
||||
|
||||
public Character.TeamType TeamID;
|
||||
|
||||
private NPCPersonalityTrait personalityTrait;
|
||||
private readonly NPCPersonalityTrait personalityTrait;
|
||||
|
||||
public Order CurrentOrder { get; set; }
|
||||
public string CurrentOrderOption { get; set; }
|
||||
@@ -400,7 +400,7 @@ namespace Barotrauma
|
||||
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
|
||||
|
||||
// Used for creating the data
|
||||
public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0)
|
||||
public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced)
|
||||
{
|
||||
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -417,12 +417,12 @@ namespace Barotrauma
|
||||
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
|
||||
if (HasGenders)
|
||||
{
|
||||
Head.gender = GetRandomGender();
|
||||
Head.gender = GetRandomGender(randSync);
|
||||
}
|
||||
Head.race = GetRandomRace();
|
||||
Head.race = GetRandomRace(randSync);
|
||||
CalculateHeadSpriteRange();
|
||||
Head.HeadSpriteId = GetRandomHeadID();
|
||||
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab, variant);
|
||||
Head.HeadSpriteId = GetRandomHeadID(randSync);
|
||||
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, variant);
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
@@ -485,7 +485,7 @@ namespace Barotrauma
|
||||
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
|
||||
if (HasGenders && gender == Gender.None)
|
||||
{
|
||||
gender = GetRandomGender();
|
||||
gender = GetRandomGender(Rand.RandSync.Unsynced);
|
||||
}
|
||||
else if (!HasGenders)
|
||||
{
|
||||
@@ -539,11 +539,9 @@ namespace Barotrauma
|
||||
LoadHeadAttachments();
|
||||
}
|
||||
|
||||
public int SetRandomHead() => HeadSpriteId = GetRandomHeadID();
|
||||
|
||||
public Gender GetRandomGender() => (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male;
|
||||
public Race GetRandomRace() => new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(Rand.RandSync.Server);
|
||||
public int GetRandomHeadID() => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, Rand.RandSync.Server) : 0;
|
||||
public Gender GetRandomGender(Rand.RandSync randSync) => (Rand.Range(0.0f, 1.0f, randSync) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male;
|
||||
public Race GetRandomRace(Rand.RandSync randSync) => new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(randSync);
|
||||
public int GetRandomHeadID(Rand.RandSync randSync) => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, randSync) : 0;
|
||||
|
||||
private List<XElement> hairs;
|
||||
private List<XElement> beards;
|
||||
@@ -670,12 +668,16 @@ namespace Barotrauma
|
||||
{
|
||||
if (HasGenders && gender == Gender.None)
|
||||
{
|
||||
gender = GetRandomGender();
|
||||
gender = GetRandomGender(Rand.RandSync.Unsynced);
|
||||
}
|
||||
else if (!HasGenders)
|
||||
{
|
||||
gender = Gender.None;
|
||||
}
|
||||
if (heads == null)
|
||||
{
|
||||
LoadHeadPresets();
|
||||
}
|
||||
head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
|
||||
CalculateHeadSpriteRange();
|
||||
ReloadHeadAttachments();
|
||||
@@ -788,7 +790,7 @@ namespace Barotrauma
|
||||
Head.FaceAttachmentIndex = faceAttachments.IndexOf(Head.FaceAttachment);
|
||||
}
|
||||
|
||||
List<XElement> AddEmpty(IEnumerable<XElement> elements, WearableType type, float commonness = 1)
|
||||
static List<XElement> AddEmpty(IEnumerable<XElement> elements, WearableType type, float commonness = 1)
|
||||
{
|
||||
// Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example.
|
||||
var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness));
|
||||
@@ -799,10 +801,9 @@ namespace Barotrauma
|
||||
|
||||
XElement GetRandomElement(IEnumerable<XElement> elements)
|
||||
{
|
||||
var filtered = elements.Where(e => IsWearableAllowed(e)).ToList();
|
||||
if (filtered.Count == 0) { return null; }
|
||||
var weights = GetWeights(filtered).ToList();
|
||||
var element = ToolBox.SelectWeightedRandom(filtered, weights, Rand.RandSync.Server);
|
||||
var filtered = elements.Where(e => IsWearableAllowed(e));
|
||||
if (filtered.Count() == 0) { return null; }
|
||||
var element = ToolBox.SelectWeightedRandom(filtered.ToList(), GetWeights(filtered).ToList(), Rand.RandSync.Unsynced);
|
||||
return element == null || element.Name == "Empty" ? null : element;
|
||||
}
|
||||
|
||||
@@ -825,8 +826,9 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsValidIndex(int index, List<XElement> list) => index >= 0 && index < list.Count;
|
||||
IEnumerable<float> GetWeights(IEnumerable<XElement> elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f));
|
||||
static bool IsValidIndex(int index, List<XElement> list) => index >= 0 && index < list.Count;
|
||||
|
||||
static IEnumerable<float> GetWeights(IEnumerable<XElement> elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -861,7 +863,7 @@ namespace Barotrauma
|
||||
|
||||
OnSkillChanged(skillIdentifier, prevLevel, newLevel, worldPos);
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && (int)newLevel != (int)prevLevel)
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !MathUtils.NearlyEqual(newLevel, prevLevel))
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills });
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace Barotrauma
|
||||
|
||||
public static void LoadAll()
|
||||
{
|
||||
foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Character))
|
||||
foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Character))
|
||||
{
|
||||
LoadFromFile(file);
|
||||
}
|
||||
|
||||
+81
-28
@@ -15,13 +15,24 @@ namespace Barotrauma
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
|
||||
|
||||
protected float _strength;
|
||||
|
||||
[Serialize(0f, true), Editable]
|
||||
public virtual float Strength
|
||||
{
|
||||
get { return _strength; }
|
||||
set { _strength = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); }
|
||||
set
|
||||
{
|
||||
if (_nonClampedStrength < 0 && value > 0)
|
||||
{
|
||||
_nonClampedStrength = value;
|
||||
}
|
||||
_strength = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength);
|
||||
}
|
||||
}
|
||||
|
||||
private float _nonClampedStrength = -1;
|
||||
public float NonClampedStrength => _nonClampedStrength > 0 ? _nonClampedStrength : _strength;
|
||||
|
||||
[Serialize("", true), Editable]
|
||||
public string Identifier { get; private set; }
|
||||
|
||||
@@ -35,6 +46,8 @@ namespace Barotrauma
|
||||
public float StrengthDiminishMultiplier = 1.0f;
|
||||
public Affliction MultiplierSource;
|
||||
|
||||
public readonly Dictionary<AfflictionPrefab.PeriodicEffect, float> PeriodicEffectTimers = new Dictionary<AfflictionPrefab.PeriodicEffect, float>();
|
||||
|
||||
/// <summary>
|
||||
/// Which character gave this affliction
|
||||
/// </summary>
|
||||
@@ -45,6 +58,11 @@ namespace Barotrauma
|
||||
Prefab = prefab;
|
||||
_strength = strength;
|
||||
Identifier = prefab?.Identifier;
|
||||
|
||||
foreach (var periodicEffect in prefab.PeriodicEffects)
|
||||
{
|
||||
PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval);
|
||||
}
|
||||
}
|
||||
|
||||
public void Serialize(XElement element)
|
||||
@@ -59,24 +77,27 @@ namespace Barotrauma
|
||||
|
||||
public Affliction CreateMultiplied(float multiplier)
|
||||
{
|
||||
return Prefab.Instantiate(Strength * multiplier, Source);
|
||||
return Prefab.Instantiate(NonClampedStrength * multiplier, Source);
|
||||
}
|
||||
|
||||
public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})";
|
||||
|
||||
public float GetVitalityDecrease(CharacterHealth characterHealth)
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) return 0.0f;
|
||||
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
if (currentEffect == null) return 0.0f;
|
||||
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) return 0.0f;
|
||||
if (currentEffect == null) { return 0.0f; }
|
||||
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; }
|
||||
|
||||
float currVitalityDecrease = MathHelper.Lerp(
|
||||
currentEffect.MinVitalityDecrease,
|
||||
currentEffect.MaxVitalityDecrease,
|
||||
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
|
||||
|
||||
if (currentEffect.MultiplyByMaxVitality) currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality;
|
||||
if (currentEffect.MultiplyByMaxVitality)
|
||||
{
|
||||
currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality;
|
||||
}
|
||||
|
||||
return currVitalityDecrease;
|
||||
}
|
||||
@@ -173,8 +194,28 @@ namespace Barotrauma
|
||||
|
||||
public virtual void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
|
||||
{
|
||||
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in Prefab.PeriodicEffects)
|
||||
{
|
||||
PeriodicEffectTimers[periodicEffect] -= deltaTime;
|
||||
if (PeriodicEffectTimers[periodicEffect] <= 0.0f)
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
PeriodicEffectTimers[periodicEffect] = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (StatusEffect statusEffect in periodicEffect.StatusEffects)
|
||||
{
|
||||
ApplyStatusEffect(statusEffect, 1.0f, characterHealth, targetLimb);
|
||||
PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
if (currentEffect == null) return;
|
||||
if (currentEffect == null) { return; }
|
||||
|
||||
if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted
|
||||
{
|
||||
@@ -184,32 +225,44 @@ namespace Barotrauma
|
||||
{
|
||||
_strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier));
|
||||
}
|
||||
// Don't use the property, because its virtual and some afflictions like husk overload it for external use.
|
||||
// Don't use the property, because it's virtual and some afflictions like husk overload it for external use.
|
||||
_strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength);
|
||||
|
||||
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
|
||||
{
|
||||
statusEffect.SetUser(Source);
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character);
|
||||
}
|
||||
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb);
|
||||
}
|
||||
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
|
||||
}
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
var targets = new List<ISerializableEntity>();
|
||||
statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets);
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targets);
|
||||
}
|
||||
ApplyStatusEffect(statusEffect, deltaTime, characterHealth, targetLimb);
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyStatusEffect(StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
|
||||
{
|
||||
statusEffect.SetUser(Source);
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character);
|
||||
}
|
||||
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb);
|
||||
}
|
||||
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
|
||||
}
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
var targets = new List<ISerializableEntity>();
|
||||
statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets);
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targets);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use this method to skip clamping and additional logic of the setters.
|
||||
/// Intended only to be used when the value is already clamped! (networking code)
|
||||
/// Ideally we would keep this private, but doing so would require too much refactoring.
|
||||
/// </summary>
|
||||
public void SetStrength(float strength) => _strength = strength;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -29,8 +29,9 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
// Don't allow to set the strength too high (from outside) to avoid rapid transformation into husk when taking lots of damage from husks.
|
||||
// If the strength is more than the value, this will effectively reset the current strength to the max. That's why we use two steps.
|
||||
float max = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1;
|
||||
float previousValue = _strength;
|
||||
float threshold = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1;
|
||||
float max = Math.Max(threshold, previousValue);
|
||||
_strength = Math.Clamp(value, 0, max);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +80,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (State != InfectionState.Active)
|
||||
{
|
||||
character.SetStun(Rand.Range(2, 4, Rand.RandSync.Server));
|
||||
character.SetStun(Rand.Range(2, 4));
|
||||
}
|
||||
State = InfectionState.Active;
|
||||
ActivateHusk();
|
||||
@@ -101,7 +102,7 @@ namespace Barotrauma
|
||||
foreach (Limb limb in character.AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
float random = Rand.Value(Rand.RandSync.Server);
|
||||
float random = Rand.Value();
|
||||
huskInfection.Clear();
|
||||
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount));
|
||||
character.LastDamageSource = null;
|
||||
|
||||
+35
-4
@@ -68,13 +68,13 @@ namespace Barotrauma
|
||||
HuskedSpeciesName = element.GetAttributeString("huskedspeciesname", null).ToLowerInvariant();
|
||||
if (HuskedSpeciesName == null)
|
||||
{
|
||||
DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange);
|
||||
DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element}", Color.Orange);
|
||||
HuskedSpeciesName = "[speciesname]husk";
|
||||
}
|
||||
TargetSpecies = element.GetAttributeStringArray("targets", new string[0] { }, trim: true, convertToLowerInvariant: true);
|
||||
if (TargetSpecies.Length == 0)
|
||||
{
|
||||
DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange);
|
||||
DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange);
|
||||
TargetSpecies = new string[] { "human" };
|
||||
}
|
||||
var attachElement = element.GetChildElement("attachlimb");
|
||||
@@ -188,6 +188,30 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public class PeriodicEffect
|
||||
{
|
||||
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
|
||||
public readonly float MinInterval, MaxInterval;
|
||||
|
||||
public PeriodicEffect(XElement element, string parentDebugName)
|
||||
{
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
|
||||
}
|
||||
|
||||
if (element.Attribute("interval") != null)
|
||||
{
|
||||
MinInterval = MaxInterval = Math.Max(element.GetAttributeFloat("interval", 1.0f), 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
MinInterval = Math.Max(element.GetAttributeFloat("mininterval", 1.0f), 1.0f);
|
||||
MaxInterval = Math.Max(element.GetAttributeFloat("maxinterval", 1.0f), MinInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static AfflictionPrefab InternalDamage;
|
||||
public static AfflictionPrefab ImpactDamage;
|
||||
public static AfflictionPrefab Bleeding;
|
||||
@@ -267,8 +291,12 @@ namespace Barotrauma
|
||||
public readonly Color[] IconColors;
|
||||
|
||||
private readonly List<Effect> effects = new List<Effect>();
|
||||
private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>();
|
||||
|
||||
public IEnumerable<Effect> Effects => effects;
|
||||
|
||||
public IList<PeriodicEffect> PeriodicEffects => periodicEffects;
|
||||
|
||||
private readonly string typeName;
|
||||
|
||||
private readonly ConstructorInfo constructor;
|
||||
@@ -304,10 +332,10 @@ namespace Barotrauma
|
||||
CharacterHealth.DamageOverlay = null;
|
||||
CharacterHealth.DamageOverlayFile = string.Empty;
|
||||
#endif
|
||||
var prevPrefabs = Prefabs.ToList();
|
||||
var prevPrefabs = Prefabs.AllPrefabs.SelectMany(kvp => kvp.Value).ToList();
|
||||
foreach (var prefab in prevPrefabs)
|
||||
{
|
||||
prefab.Dispose();
|
||||
prefab?.Dispose();
|
||||
}
|
||||
System.Diagnostics.Debug.Assert(Prefabs.Count() == 0, "All previous AfflictionPrefabs were not removed in AfflictionPrefab.LoadAll");
|
||||
|
||||
@@ -552,6 +580,9 @@ namespace Barotrauma
|
||||
case "effect":
|
||||
effects.Add(new Effect(subElement, Name));
|
||||
break;
|
||||
case "periodiceffect":
|
||||
periodicEffects.Add(new PeriodicEffect(subElement, Name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -170,12 +170,12 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Character.NeedsOxygen || Unkillable) { return 100.0f; }
|
||||
if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return 100.0f; }
|
||||
return -oxygenLowAffliction.Strength + 100;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (!Character.NeedsOxygen || Unkillable) { return; }
|
||||
if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return; }
|
||||
oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f);
|
||||
}
|
||||
}
|
||||
@@ -399,7 +399,7 @@ namespace Barotrauma
|
||||
|
||||
public void ApplyAffliction(Limb targetLimb, Affliction affliction)
|
||||
{
|
||||
if (Unkillable) { return; }
|
||||
if (Unkillable || Character.GodMode) { return; }
|
||||
if (affliction.Prefab.LimbSpecific)
|
||||
{
|
||||
if (targetLimb == null)
|
||||
@@ -481,7 +481,7 @@ namespace Barotrauma
|
||||
|
||||
public void ApplyDamage(Limb hitLimb, AttackResult attackResult)
|
||||
{
|
||||
if (Unkillable) { return; }
|
||||
if (Unkillable || Character.GodMode) { return; }
|
||||
if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count)
|
||||
{
|
||||
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
|
||||
@@ -504,7 +504,7 @@ namespace Barotrauma
|
||||
|
||||
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
|
||||
{
|
||||
if (Unkillable) { return; }
|
||||
if (Unkillable || Character.GodMode) { return; }
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
limbHealth.Afflictions.RemoveAll(a =>
|
||||
@@ -741,7 +741,7 @@ namespace Barotrauma
|
||||
public void CalculateVitality()
|
||||
{
|
||||
Vitality = MaxVitality;
|
||||
if (Unkillable) { return; }
|
||||
if (Unkillable || Character.GodMode) { return; }
|
||||
|
||||
float damageResistanceMultiplier = 1f - GetResistance("damage");
|
||||
|
||||
@@ -777,7 +777,7 @@ namespace Barotrauma
|
||||
|
||||
private void Kill()
|
||||
{
|
||||
if (Unkillable) { return; }
|
||||
if (Unkillable || Character.GodMode) { return; }
|
||||
|
||||
var causeOfDeath = GetCauseOfDeath();
|
||||
Character.Kill(causeOfDeath.First, causeOfDeath.Second);
|
||||
@@ -913,6 +913,11 @@ namespace Barotrauma
|
||||
msg.WriteRangedSingle(
|
||||
MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
|
||||
0.0f, affliction.Prefab.MaxStrength, 8);
|
||||
msg.Write((byte)affliction.Prefab.PeriodicEffects.Count());
|
||||
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects)
|
||||
{
|
||||
msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8);
|
||||
}
|
||||
}
|
||||
|
||||
limbAfflictions.Clear();
|
||||
@@ -933,6 +938,11 @@ namespace Barotrauma
|
||||
msg.WriteRangedSingle(
|
||||
MathHelper.Clamp(limbAffliction.Second.Strength, 0.0f, limbAffliction.Second.Prefab.MaxStrength),
|
||||
0.0f, limbAffliction.Second.Prefab.MaxStrength, 8);
|
||||
msg.Write((byte)limbAffliction.Second.Prefab.PeriodicEffects.Count());
|
||||
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in limbAffliction.Second.Prefab.PeriodicEffects)
|
||||
{
|
||||
msg.WriteRangedSingle(limbAffliction.Second.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,10 @@ namespace Barotrauma
|
||||
if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck")
|
||||
{
|
||||
item.AddTag("name:" + character.Name);
|
||||
item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine));
|
||||
if (Level.Loaded != null)
|
||||
{
|
||||
item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine));
|
||||
}
|
||||
var job = character.Info?.Job;
|
||||
if (job != null)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace Barotrauma
|
||||
{
|
||||
public string identifier;
|
||||
public string option;
|
||||
public float priorityModifier;
|
||||
public readonly float priorityModifier;
|
||||
public readonly bool ignoreAtOutpost;
|
||||
|
||||
public AutonomousObjective(XElement element)
|
||||
{
|
||||
@@ -26,6 +27,7 @@ namespace Barotrauma
|
||||
option = element.GetAttributeString("option", null);
|
||||
priorityModifier = element.GetAttributeFloat("prioritymodifier", 1);
|
||||
priorityModifier = MathHelper.Max(priorityModifier, 0);
|
||||
ignoreAtOutpost = element.GetAttributeBool("ignoreatoutpost", false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +66,7 @@ namespace Barotrauma
|
||||
public readonly Dictionary<int, List<string>> ItemIdentifiers = new Dictionary<int, List<string>>();
|
||||
public readonly Dictionary<int, Dictionary<string, bool>> ShowItemPreview = new Dictionary<int, Dictionary<string, bool>>();
|
||||
public readonly List<SkillPrefab> Skills = new List<SkillPrefab>();
|
||||
public readonly List<AutonomousObjective> AutonomousObjective = new List<AutonomousObjective>();
|
||||
public readonly List<AutonomousObjective> AutonomousObjectives = new List<AutonomousObjective>();
|
||||
public readonly List<string> AppropriateOrders = new List<string>();
|
||||
|
||||
[Serialize("1,1,1,1", false)]
|
||||
@@ -209,7 +211,7 @@ namespace Barotrauma
|
||||
}
|
||||
break;
|
||||
case "autonomousobjectives":
|
||||
subElement.Elements().ForEach(order => AutonomousObjective.Add(new AutonomousObjective(order)));
|
||||
subElement.Elements().ForEach(order => AutonomousObjectives.Add(new AutonomousObjective(order)));
|
||||
break;
|
||||
case "appropriateobjectives":
|
||||
case "appropriateorders":
|
||||
|
||||
@@ -639,6 +639,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)
|
||||
{
|
||||
@@ -646,6 +647,7 @@ namespace Barotrauma
|
||||
afflictionsCopy.Clear();
|
||||
foreach (var affliction in afflictions)
|
||||
{
|
||||
tempModifiers.Clear();
|
||||
var newAffliction = affliction;
|
||||
float random = Rand.Value(Rand.RandSync.Unsynced);
|
||||
if (random > affliction.Probability) { continue; }
|
||||
@@ -660,8 +662,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
|
||||
{
|
||||
newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier);
|
||||
appliedDamageModifiers.Add(damageModifier);
|
||||
tempModifiers.Add(damageModifier);
|
||||
}
|
||||
}
|
||||
foreach (WearableSprite wearable in wearingItems)
|
||||
@@ -676,18 +677,49 @@ namespace Barotrauma
|
||||
}
|
||||
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
|
||||
{
|
||||
newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier);
|
||||
appliedDamageModifiers.Add(damageModifier);
|
||||
tempModifiers.Add(damageModifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
float finalDamageModifier = 1.0f;
|
||||
foreach (DamageModifier damageModifier in tempModifiers)
|
||||
{
|
||||
finalDamageModifier *= damageModifier.DamageMultiplier;
|
||||
}
|
||||
if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f))
|
||||
{
|
||||
newAffliction = affliction.CreateMultiplied(finalDamageModifier);
|
||||
}
|
||||
|
||||
if (applyAffliction)
|
||||
{
|
||||
afflictionsCopy.Add(newAffliction);
|
||||
}
|
||||
appliedDamageModifiers.AddRange(tempModifiers);
|
||||
}
|
||||
var result = new AttackResult(afflictionsCopy, this, appliedDamageModifiers);
|
||||
AddDamageProjSpecific(playSound, result);
|
||||
|
||||
float bleedingDamage = 0;
|
||||
if (character.CharacterHealth.DoesBleed)
|
||||
{
|
||||
foreach (var affliction in result.Afflictions)
|
||||
{
|
||||
if (affliction is AfflictionBleeding)
|
||||
{
|
||||
bleedingDamage += affliction.GetVitalityDecrease(character.CharacterHealth);
|
||||
}
|
||||
}
|
||||
if (bleedingDamage > 0)
|
||||
{
|
||||
float bloodDecalSize = MathHelper.Clamp(bleedingDamage / 5, 0.1f, 1.0f);
|
||||
if (character.CurrentHull != null && !string.IsNullOrEmpty(character.BloodDecalName))
|
||||
{
|
||||
character.CurrentHull.AddDecal(character.BloodDecalName, WorldPosition, MathHelper.Clamp(bloodDecalSize, 0.5f, 1.0f), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -751,7 +783,7 @@ namespace Barotrauma
|
||||
Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition;
|
||||
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
|
||||
bool wasRunning = attack.IsRunning;
|
||||
attack.UpdateAttackTimer(deltaTime);
|
||||
attack.UpdateAttackTimer(deltaTime, character);
|
||||
|
||||
bool wasHit = false;
|
||||
Body structureBody = null;
|
||||
@@ -762,7 +794,11 @@ namespace Barotrauma
|
||||
case HitDetection.Distance:
|
||||
if (dist < attack.DamageRange)
|
||||
{
|
||||
structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
|
||||
structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
|
||||
if (structureBody?.UserData as string == "ruinroom")
|
||||
{
|
||||
structureBody = null;
|
||||
}
|
||||
if (damageTarget is Item i && i.GetComponent<Items.Components.Door>() != null)
|
||||
{
|
||||
// If the attack is aimed to an item and hits an item, it's successful.
|
||||
@@ -917,7 +953,7 @@ namespace Barotrauma
|
||||
StickTo(structureBody, from, to);
|
||||
}*/
|
||||
attack.ResetAttackTimer();
|
||||
attack.SetCoolDown();
|
||||
attack.SetCoolDown(applyRandom: !character.IsPlayer);
|
||||
}
|
||||
|
||||
private WeldJoint attachJoint;
|
||||
|
||||
+10
-7
@@ -20,17 +20,17 @@ namespace Barotrauma
|
||||
|
||||
abstract class GroundedMovementParams : AnimationParams
|
||||
{
|
||||
[Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2)]
|
||||
[Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)]
|
||||
public Vector2 StepSize
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2)]
|
||||
[Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
|
||||
public float HeadPosition { get; set; }
|
||||
|
||||
[Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2)]
|
||||
[Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
|
||||
public float TorsoPosition { get; set; }
|
||||
|
||||
[Serialize(1f, true, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)]
|
||||
@@ -39,7 +39,10 @@ namespace Barotrauma
|
||||
[Serialize(0f, true, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)]
|
||||
public float StepLiftAmount { get; set; }
|
||||
|
||||
[Serialize(-0.5f, true, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)]
|
||||
[Serialize(true, true), Editable]
|
||||
public bool MultiplyByDir { get; set; }
|
||||
|
||||
[Serialize(0.5f, true, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)]
|
||||
public float StepLiftOffset { get; set; }
|
||||
|
||||
[Serialize(2f, true, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)]
|
||||
@@ -51,7 +54,7 @@ namespace Barotrauma
|
||||
|
||||
abstract class SwimParams : AnimationParams
|
||||
{
|
||||
[Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
[Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 500, ValueStep = 1)]
|
||||
public float SteerTorque { get; set; }
|
||||
}
|
||||
|
||||
@@ -63,11 +66,11 @@ namespace Barotrauma
|
||||
|
||||
protected static Dictionary<string, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<string, Dictionary<string, AnimationParams>>();
|
||||
|
||||
[Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED)]
|
||||
[Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
|
||||
public float MovementSpeed { get; set; }
|
||||
|
||||
[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)]
|
||||
Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)]
|
||||
public float CycleSpeed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
+9
-9
@@ -87,19 +87,19 @@ namespace Barotrauma
|
||||
[Serialize(8.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
|
||||
public float FootMoveForce { get; set; }
|
||||
|
||||
[Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
[Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
||||
public float HeadTorque { get; set; }
|
||||
|
||||
[Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
[Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
||||
public float TorsoTorque { get; set; }
|
||||
|
||||
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
||||
public float TailTorque { get; set; }
|
||||
|
||||
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
||||
public float FootTorque { get; set; }
|
||||
|
||||
[Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
[Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
public float LegTorque { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -173,19 +173,19 @@ namespace Barotrauma
|
||||
[Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")]
|
||||
public bool RotateTowardsMovement { get; set; }
|
||||
|
||||
[Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
[Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
||||
public float TorsoTorque { get; set; }
|
||||
|
||||
[Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
[Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
||||
public float HeadTorque { get; set; }
|
||||
|
||||
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
||||
public float TailTorque { get; set; }
|
||||
|
||||
[Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)]
|
||||
public float TailTorqueMultiplier { get; set; }
|
||||
|
||||
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
||||
public float FootTorque { get; set; }
|
||||
|
||||
[Serialize(null, true), Editable]
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Steam;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -23,7 +24,6 @@ namespace Barotrauma
|
||||
NPCSets,
|
||||
Factions,
|
||||
Text,
|
||||
Executable,
|
||||
ServerExecutable,
|
||||
LocationTypes,
|
||||
MapGenerationParameters,
|
||||
@@ -54,7 +54,22 @@ namespace Barotrauma
|
||||
{
|
||||
public static string Folder = "Data/ContentPackages/";
|
||||
|
||||
public static List<ContentPackage> List = new List<ContentPackage>();
|
||||
private static List<ContentPackage> regularPackages = new List<ContentPackage>();
|
||||
public static IReadOnlyList<ContentPackage> RegularPackages
|
||||
{
|
||||
get { return regularPackages; }
|
||||
}
|
||||
|
||||
private static List<ContentPackage> corePackages = new List<ContentPackage>();
|
||||
public static IReadOnlyList<ContentPackage> CorePackages
|
||||
{
|
||||
get { return corePackages; }
|
||||
}
|
||||
|
||||
public static IEnumerable<ContentPackage> AllPackages
|
||||
{
|
||||
get { return corePackages.Concat(regularPackages); }
|
||||
}
|
||||
|
||||
//these types of files are included in the MD5 hash calculation,
|
||||
//meaning that the players must have the exact same files to play together
|
||||
@@ -97,7 +112,6 @@ namespace Barotrauma
|
||||
ContentType.Wreck,
|
||||
ContentType.WreckAIConfig,
|
||||
ContentType.Text,
|
||||
ContentType.Executable,
|
||||
ContentType.ServerExecutable,
|
||||
ContentType.LocationTypes,
|
||||
ContentType.MapGenerationParameters,
|
||||
@@ -128,7 +142,7 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
public string SteamWorkshopUrl;
|
||||
public ulong SteamWorkshopId;
|
||||
public DateTime? InstallTime;
|
||||
|
||||
public bool HideInWorkshopMenu
|
||||
@@ -160,10 +174,24 @@ namespace Barotrauma
|
||||
//core packages are content packages that are required for the game to work
|
||||
//e.g. they include the executable, some location types, level generation params and other files the game won't work without
|
||||
//one (and only one) core package must always be selected
|
||||
public bool CorePackage
|
||||
private bool isCorePackage;
|
||||
public bool IsCorePackage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get { return isCorePackage; }
|
||||
set
|
||||
{
|
||||
isCorePackage = value;
|
||||
if (isCorePackage && regularPackages.Contains(this))
|
||||
{
|
||||
corePackages.Add(this);
|
||||
regularPackages.Remove(this);
|
||||
}
|
||||
else if (!isCorePackage && corePackages.Contains(this))
|
||||
{
|
||||
regularPackages.Add(this);
|
||||
corePackages.Remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Version GameVersion
|
||||
@@ -171,7 +199,31 @@ namespace Barotrauma
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<ContentFile> Files;
|
||||
|
||||
private List<ContentFile> files;
|
||||
private List<ContentFile> filesToAdd;
|
||||
private List<ContentFile> filesToRemove;
|
||||
|
||||
|
||||
public IReadOnlyList<ContentFile> Files
|
||||
{
|
||||
get { return files; }
|
||||
}
|
||||
|
||||
public IEnumerable<ContentFile> FilesUnsaved
|
||||
{
|
||||
get { return files.Where(f => !filesToRemove.Contains(f)).Concat(filesToAdd); }
|
||||
}
|
||||
|
||||
public IReadOnlyList<ContentFile> FilesToAdd
|
||||
{
|
||||
get { return filesToAdd; }
|
||||
}
|
||||
|
||||
public IReadOnlyList<ContentFile> FilesToRemove
|
||||
{
|
||||
get { return filesToRemove; }
|
||||
}
|
||||
|
||||
public bool HasMultiplayerIncompatibleContent
|
||||
{
|
||||
@@ -180,7 +232,9 @@ namespace Barotrauma
|
||||
|
||||
private ContentPackage()
|
||||
{
|
||||
Files = new List<ContentFile>();
|
||||
files = new List<ContentFile>();
|
||||
filesToAdd = new List<ContentFile>();
|
||||
filesToRemove = new List<ContentFile>();
|
||||
}
|
||||
|
||||
public ContentPackage(string filePath, string setPath = "")
|
||||
@@ -200,8 +254,13 @@ namespace Barotrauma
|
||||
|
||||
Name = doc.Root.GetAttributeString("name", "");
|
||||
HideInWorkshopMenu = doc.Root.GetAttributeBool("hideinworkshopmenu", false);
|
||||
CorePackage = doc.Root.GetAttributeBool("corepackage", false);
|
||||
SteamWorkshopUrl = doc.Root.GetAttributeString("steamworkshopurl", "");
|
||||
isCorePackage = doc.Root.GetAttributeBool("corepackage", false);
|
||||
SteamWorkshopId = doc.Root.GetAttributeUInt64("steamworkshopid", 0);
|
||||
string workshopUrl = doc.Root.GetAttributeString("steamworkshopurl", "");
|
||||
if (!string.IsNullOrEmpty(workshopUrl))
|
||||
{
|
||||
SteamWorkshopId = SteamManager.GetWorkshopItemIDFromUrl(workshopUrl);
|
||||
}
|
||||
GameVersion = new Version(doc.Root.GetAttributeString("gameversion", "0.0.0.0"));
|
||||
if (doc.Root.Attribute("installtime") != null)
|
||||
{
|
||||
@@ -211,12 +270,13 @@ namespace Barotrauma
|
||||
List<string> errorMsgs = new List<string>();
|
||||
foreach (XElement subElement in doc.Root.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().Equals("executable", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
if (!Enum.TryParse(subElement.Name.ToString(), true, out ContentType type))
|
||||
{
|
||||
errorMsgs.Add("Error in content package \"" + Name + "\" - \"" + subElement.Name.ToString() + "\" is not a valid content type.");
|
||||
type = ContentType.None;
|
||||
}
|
||||
Files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type, this));
|
||||
files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type, this));
|
||||
}
|
||||
|
||||
if (Files.Count == 0)
|
||||
@@ -227,7 +287,7 @@ namespace Barotrauma
|
||||
string folder = System.IO.Path.GetDirectoryName(filePath);
|
||||
if (File.Exists(System.IO.Path.Combine(folder, Name+".sub")))
|
||||
{
|
||||
Files.Add(new ContentFile(System.IO.Path.Combine(folder, Name + ".sub"), ContentType.Submarine, this));
|
||||
files.Add(new ContentFile(System.IO.Path.Combine(folder, Name + ".sub"), ContentType.Submarine, this));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -318,7 +378,6 @@ namespace Barotrauma
|
||||
{
|
||||
switch (file.Type)
|
||||
{
|
||||
case ContentType.Executable:
|
||||
case ContentType.ServerExecutable:
|
||||
case ContentType.None:
|
||||
case ContentType.Outpost:
|
||||
@@ -351,7 +410,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (CorePackage && !ContainsRequiredCorePackageFiles(out List<ContentType> missingContentTypes))
|
||||
if (IsCorePackage && !ContainsRequiredCorePackageFiles(out List<ContentType> missingContentTypes))
|
||||
{
|
||||
errorMessages.Add(TextManager.GetWithVariables("ContentPackageCantMakeCorePackage",
|
||||
new string[2] { "[packagename]", "[missingfiletypes]" },
|
||||
@@ -375,7 +434,6 @@ namespace Barotrauma
|
||||
foreach (ContentFile file in Files)
|
||||
{
|
||||
//TODO: determine executable extension on platform and check for the presence of the executables
|
||||
if (file.Type == ContentType.Executable) { continue; }
|
||||
if (file.Type == ContentType.ServerExecutable) { continue; }
|
||||
|
||||
if (!File.Exists(file.Path))
|
||||
@@ -394,7 +452,7 @@ namespace Barotrauma
|
||||
{
|
||||
Name = name,
|
||||
Path = path,
|
||||
CorePackage = corePackage,
|
||||
isCorePackage = corePackage,
|
||||
GameVersion = GameMain.Version
|
||||
};
|
||||
|
||||
@@ -403,35 +461,91 @@ namespace Barotrauma
|
||||
|
||||
public ContentFile AddFile(string path, ContentType type)
|
||||
{
|
||||
if (Files.Find(file => file.Path == path && file.Type == type) != null) return null;
|
||||
if (Files.Concat(FilesToAdd).Any(file => file.Path == path && file.Type == type)) return null;
|
||||
|
||||
ContentFile cf = new ContentFile(path, type)
|
||||
{
|
||||
ContentPackage = this
|
||||
};
|
||||
Files.Add(cf);
|
||||
filesToAdd.Add(cf);
|
||||
|
||||
return cf;
|
||||
}
|
||||
|
||||
public void RemoveFile(ContentFile file)
|
||||
public void AddFile(ContentFile file)
|
||||
{
|
||||
Files.Remove(file);
|
||||
if (filesToRemove.Contains(file)) { filesToRemove.Remove(file); }
|
||||
if (Files.Concat(FilesToAdd).Any(f => f.Path == file.Path && f.Type == file.Type)) return;
|
||||
|
||||
filesToAdd.Add(file);
|
||||
}
|
||||
|
||||
public void Save(string filePath)
|
||||
public void RemoveFile(ContentFile file)
|
||||
{
|
||||
if (filesToAdd.Contains(file)) { filesToAdd.Remove(file); }
|
||||
if (files.Contains(file) && !filesToRemove.Contains(file)) { filesToRemove.Add(file); }
|
||||
}
|
||||
|
||||
public void Save(string filePath, bool reload = true)
|
||||
{
|
||||
var packagesToDeselect = corePackages.Concat(regularPackages).Where(p => p.Path.CleanUpPath() == Path.CleanUpPath()).ToList();
|
||||
bool refreshFiles = false;
|
||||
|
||||
if (packagesToDeselect.Any())
|
||||
{
|
||||
foreach (var p in packagesToDeselect)
|
||||
{
|
||||
if (p.IsCorePackage)
|
||||
{
|
||||
if (GameMain.Config.CurrentCorePackage == p)
|
||||
{
|
||||
refreshFiles = true;
|
||||
}
|
||||
corePackages.Remove(p);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GameMain.Config.EnabledRegularPackages.Contains(p))
|
||||
{
|
||||
refreshFiles = true;
|
||||
}
|
||||
regularPackages.Remove(p);
|
||||
}
|
||||
}
|
||||
if (IsCorePackage)
|
||||
{
|
||||
corePackages.Add(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
regularPackages.Add(this);
|
||||
}
|
||||
|
||||
if (refreshFiles)
|
||||
{
|
||||
GameMain.Config.DisableContentPackageItems(filesToRemove);
|
||||
GameMain.Config.EnableContentPackageItems(filesToAdd);
|
||||
GameMain.Config.RefreshContentPackageItems(filesToRemove.Concat(filesToAdd).Distinct());
|
||||
}
|
||||
}
|
||||
files.RemoveAll(f => filesToRemove.Contains(f));
|
||||
files.AddRange(filesToAdd);
|
||||
filesToRemove.Clear(); filesToAdd.Clear();
|
||||
|
||||
XDocument doc = new XDocument();
|
||||
doc.Add(new XElement("contentpackage",
|
||||
new XAttribute("name", Name),
|
||||
new XAttribute("path", Path.CleanUpPathCrossPlatform(correctFilenameCase: false)),
|
||||
new XAttribute("corepackage", CorePackage)));
|
||||
new XAttribute("corepackage", IsCorePackage)));
|
||||
|
||||
doc.Root.Add(new XAttribute("gameversion", GameVersion.ToString()));
|
||||
|
||||
if (!string.IsNullOrEmpty(SteamWorkshopUrl))
|
||||
if (SteamWorkshopId != 0)
|
||||
{
|
||||
doc.Root.Add(new XAttribute("steamworkshopurl", SteamWorkshopUrl));
|
||||
doc.Root.Add(new XAttribute("steamworkshopid", SteamWorkshopId.ToString()));
|
||||
#if UNSTABLE
|
||||
doc.Root.Add(new XAttribute("steamworkshopurl", $"http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={SteamWorkshopId}"));
|
||||
#endif
|
||||
}
|
||||
|
||||
if (InstallTime != null)
|
||||
@@ -445,41 +559,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
doc.SaveSafe(filePath);
|
||||
|
||||
var packagesToDeselect = List.Where(p => p.Path.CleanUpPath() == Path.CleanUpPath()).ToList();
|
||||
bool reselectPackage = false;
|
||||
|
||||
if (packagesToDeselect.Any())
|
||||
{
|
||||
foreach (var p in packagesToDeselect)
|
||||
{
|
||||
if (GameMain.Config.SelectedContentPackages.Contains(p))
|
||||
{
|
||||
reselectPackage = true;
|
||||
if (p.CorePackage)
|
||||
{
|
||||
GameMain.Config.AutoSelectCorePackage(packagesToDeselect);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.Config.DeselectContentPackage(p);
|
||||
}
|
||||
}
|
||||
List.Remove(p);
|
||||
}
|
||||
List.Add(this);
|
||||
if (reselectPackage)
|
||||
{
|
||||
if (CorePackage)
|
||||
{
|
||||
GameMain.Config.SelectCorePackage(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.Config.SelectContentPackage(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CalculateHash(bool logging = false)
|
||||
@@ -633,9 +712,31 @@ namespace Barotrauma
|
||||
return Files.Where(f => f.Type == type).Select(f => f.Path);
|
||||
}
|
||||
|
||||
public static void AddPackage(ContentPackage newPackage)
|
||||
{
|
||||
if (corePackages.Concat(regularPackages).Any(p => p.Name.Equals(newPackage.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
DebugConsole.ThrowError($"Attempted to add \"{newPackage.Name}\" more than once!\n{Environment.StackTrace}");
|
||||
}
|
||||
if (newPackage.IsCorePackage)
|
||||
{
|
||||
corePackages.Add(newPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
regularPackages.Add(newPackage);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemovePackage(ContentPackage package)
|
||||
{
|
||||
if (package.IsCorePackage) { corePackages.Remove(package); }
|
||||
else { regularPackages.Remove(package); }
|
||||
}
|
||||
|
||||
public static void LoadAll()
|
||||
{
|
||||
string folder = ContentPackage.Folder;
|
||||
string folder = Folder;
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
try
|
||||
@@ -651,11 +752,13 @@ namespace Barotrauma
|
||||
|
||||
IEnumerable<string> files = Directory.GetFiles(folder, "*.xml");
|
||||
|
||||
List.Clear();
|
||||
corePackages.Clear();
|
||||
var prevRegularPackages = regularPackages.Select(p => p.Name.ToLowerInvariant()).ToList();
|
||||
regularPackages.Clear();
|
||||
|
||||
foreach (string filePath in files)
|
||||
{
|
||||
List.Add(new ContentPackage(filePath));
|
||||
AddPackage(new ContentPackage(filePath));
|
||||
}
|
||||
|
||||
IEnumerable<string> modDirectories = Directory.GetDirectories("Mods");
|
||||
@@ -671,54 +774,39 @@ namespace Barotrauma
|
||||
}
|
||||
else if (File.Exists(modFilePath))
|
||||
{
|
||||
List.Add(new ContentPackage(modFilePath));
|
||||
AddPackage(new ContentPackage(modFilePath));
|
||||
}
|
||||
}
|
||||
|
||||
List = List
|
||||
.OrderByDescending(p => p.CorePackage)
|
||||
.ThenByDescending(p => GameMain.Config?.SelectedContentPackages.Contains(p))
|
||||
.ThenBy(p => GameMain.Config?.SelectedContentPackages.IndexOf(p))
|
||||
.ToList();
|
||||
SortContentPackages(p => prevRegularPackages.IndexOf(p.Name.ToLowerInvariant()));
|
||||
GameMain.Config?.SortContentPackages();
|
||||
}
|
||||
|
||||
public static void SortContentPackages()
|
||||
public static void SortContentPackages<T>(Func<ContentPackage, T> order, bool refreshAll = false)
|
||||
{
|
||||
if (GameMain.Config != null)
|
||||
{
|
||||
List = List
|
||||
.OrderByDescending(p => p.CorePackage)
|
||||
.ThenBy(p => GameMain.Config.SelectedContentPackages.IndexOf(p))
|
||||
.ThenBy(p => List.IndexOf(p))
|
||||
.ToList();
|
||||
|
||||
var sortedSelected = GameMain.Config.SelectedContentPackages
|
||||
.OrderByDescending(p => p.CorePackage)
|
||||
.ThenBy(p => GameMain.Config.SelectedContentPackages.IndexOf(p))
|
||||
.ToList();
|
||||
GameMain.Config.SelectedContentPackages.Clear(); GameMain.Config.SelectedContentPackages.AddRange(sortedSelected);
|
||||
|
||||
var reportList = GameMain.Config.SelectedContentPackages;
|
||||
DebugConsole.NewMessage($"Content package load order: { string.Join(" | ", reportList.Select(cp => cp.Name)) }");
|
||||
}
|
||||
else
|
||||
{
|
||||
List = List
|
||||
.OrderByDescending(p => p.CorePackage)
|
||||
.ThenBy(p => List.IndexOf(p))
|
||||
.ToList();
|
||||
}
|
||||
var ordered = regularPackages
|
||||
.OrderBy(p => order(p))
|
||||
.ThenBy(p => regularPackages.IndexOf(p))
|
||||
.ToList();
|
||||
regularPackages.Clear(); regularPackages.AddRange(ordered);
|
||||
GameMain.Config?.SortContentPackages(refreshAll);
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
try
|
||||
{
|
||||
GameMain.Config.DeselectContentPackage(this);
|
||||
if (IsCorePackage)
|
||||
{
|
||||
corePackages.Remove(this);
|
||||
if (GameMain.Config.CurrentCorePackage == this) { GameMain.Config.AutoSelectCorePackage(null); }
|
||||
}
|
||||
else
|
||||
{
|
||||
regularPackages.Remove(this);
|
||||
if (GameMain.Config.EnabledRegularPackages.Contains(this)) { GameMain.Config.DisableRegularPackage(this); }
|
||||
}
|
||||
GameMain.Config.SaveNewPlayerConfig();
|
||||
List.Remove(this);
|
||||
File.Delete(Path);
|
||||
SortContentPackages();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -258,7 +258,7 @@ namespace Barotrauma
|
||||
{
|
||||
HumanAIController.DisableCrewAI = true;
|
||||
NewMessage("Crew AI disabled", Color.Red);
|
||||
}));
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("enablecrewai", "enablecrewai: Enable the AI of the NPCs in the crew.", (string[] args) =>
|
||||
{
|
||||
@@ -509,7 +509,21 @@ namespace Barotrauma
|
||||
return new string[][] { ListCharacterNames() };
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("godmode", "godmode: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) =>
|
||||
commands.Add(new Command("godmode", "godmode [character name]: Toggle character godmode. Makes the targeted character invulnerable to damage. If the name parameter is omitted, the controlled character will receive godmode.",
|
||||
(string[] args) =>
|
||||
{
|
||||
Character targetCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false);
|
||||
|
||||
if (targetCharacter == null) { return; }
|
||||
|
||||
targetCharacter.GodMode = !targetCharacter.GodMode;
|
||||
},
|
||||
() =>
|
||||
{
|
||||
return new string[][] { ListCharacterNames() };
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("godmode_mainsub", "godmode_mainsub: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) =>
|
||||
{
|
||||
if (Submarine.MainSub == null) return;
|
||||
|
||||
@@ -517,6 +531,17 @@ namespace Barotrauma
|
||||
NewMessage(Submarine.MainSub.GodMode ? "Godmode on" : "Godmode off", Color.White);
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("growthdelay", "growthdelay: Sets how long it takes for planters to attempt to advance a plant's growth.", (string[] args) =>
|
||||
{
|
||||
if (args.Length > 0 && float.TryParse(args[0], out float value))
|
||||
{
|
||||
Planter.GrowthTickDelay = value;
|
||||
NewMessage($"Growth delay set to {value}.", Color.Green);
|
||||
return;
|
||||
}
|
||||
NewMessage("Invalid value.", Color.Red);
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("lock", "lock: Lock movement of the main submarine.", (string[] args) =>
|
||||
{
|
||||
Submarine.LockX = !Submarine.LockX;
|
||||
@@ -1208,6 +1233,17 @@ namespace Barotrauma
|
||||
}
|
||||
}));
|
||||
|
||||
commands.Add(new Command("togglecampaignteleport", "Toggle on/off teleportation between campaign locations by double clicking on the campaign map.", args =>
|
||||
{
|
||||
if (GameMain.GameSession?.Campaign == null)
|
||||
{
|
||||
ThrowError("No campaign active.");
|
||||
return;
|
||||
}
|
||||
GameMain.GameSession.Map.AllowDebugTeleport = !GameMain.GameSession.Map.AllowDebugTeleport;
|
||||
NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", Color.White);
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("money", "", args =>
|
||||
{
|
||||
if (args.Length == 0) { return; }
|
||||
@@ -1238,14 +1274,14 @@ namespace Barotrauma
|
||||
NewMessage((GameSettings.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White);
|
||||
}, isCheat: false));
|
||||
|
||||
commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", TaskPool.ListTasks));
|
||||
commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(); }));
|
||||
|
||||
commands.Add(new Command("calculatehashes", "calculatehashes [content package name]: Show the MD5 hashes of the files in the selected content package. If the name parameter is omitted, the first content package is selected.", (string[] args) =>
|
||||
{
|
||||
if (args.Length > 0)
|
||||
{
|
||||
string packageName = string.Join(" ", args).ToLower();
|
||||
var package = GameMain.Config.SelectedContentPackages.FirstOrDefault(p => p.Name.ToLower() == packageName);
|
||||
var package = GameMain.Config.AllEnabledPackages.FirstOrDefault(p => p.Name.ToLower() == packageName);
|
||||
if (package == null)
|
||||
{
|
||||
ThrowError("Content package \"" + packageName + "\" not found.");
|
||||
@@ -1257,14 +1293,14 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.Config.SelectedContentPackages.First().CalculateHash(logging: true);
|
||||
GameMain.Config.AllEnabledPackages.First().CalculateHash(logging: true);
|
||||
}
|
||||
},
|
||||
() =>
|
||||
{
|
||||
return new string[][]
|
||||
{
|
||||
GameMain.Config.SelectedContentPackages.Select(cp => cp.Name).ToArray()
|
||||
GameMain.Config.AllEnabledPackages.Select(cp => cp.Name).ToArray()
|
||||
};
|
||||
}));
|
||||
|
||||
@@ -1859,7 +1895,7 @@ namespace Barotrauma
|
||||
if (GameSettings.VerboseLogging) NewMessage(message, Color.Gray);
|
||||
}
|
||||
|
||||
public static void ThrowError(string error, Exception e = null, bool createMessageBox = false)
|
||||
public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false)
|
||||
{
|
||||
if (e != null)
|
||||
{
|
||||
@@ -1869,6 +1905,10 @@ namespace Barotrauma
|
||||
error += "\n\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace;
|
||||
}
|
||||
}
|
||||
else if (appendStackTrace)
|
||||
{
|
||||
error += "\n" + Environment.StackTrace;
|
||||
}
|
||||
System.Diagnostics.Debug.WriteLine(error);
|
||||
|
||||
#if CLIENT
|
||||
@@ -1888,27 +1928,7 @@ namespace Barotrauma
|
||||
public static void AddWarning(string warning)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(warning);
|
||||
#if CLIENT
|
||||
if (listBox == null) { NewMessage($"WARNING: {warning}", Color.Yellow); return; }
|
||||
|
||||
var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), style: "InnerFrame", color: Color.White)
|
||||
{
|
||||
CanBeFocused = false
|
||||
};
|
||||
var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) },
|
||||
warning, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true)
|
||||
{
|
||||
CanBeFocused = false,
|
||||
TextColor = Color.Yellow
|
||||
};
|
||||
textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5);
|
||||
textBlock.SetTextPos();
|
||||
|
||||
listBox.UpdateScrollBarSize();
|
||||
listBox.BarScroll = 1.0f;
|
||||
#else
|
||||
NewMessage($"WARNING: {warning}", Color.Yellow);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class Decal
|
||||
{
|
||||
public readonly DecalPrefab Prefab;
|
||||
private Vector2 position;
|
||||
|
||||
private float fadeTimer;
|
||||
|
||||
public readonly Sprite Sprite;
|
||||
|
||||
public float FadeTimer
|
||||
{
|
||||
get { return fadeTimer; }
|
||||
set { fadeTimer = MathHelper.Clamp(value, 0.0f, LifeTime); }
|
||||
}
|
||||
|
||||
public float FadeInTime
|
||||
{
|
||||
get { return Prefab.FadeInTime; }
|
||||
}
|
||||
|
||||
public float FadeOutTime
|
||||
{
|
||||
get { return Prefab.FadeOutTime; }
|
||||
}
|
||||
|
||||
public float LifeTime
|
||||
{
|
||||
get { return Prefab.LifeTime; }
|
||||
}
|
||||
|
||||
private float baseAlpha = 1.0f;
|
||||
public float BaseAlpha
|
||||
{
|
||||
get { return baseAlpha; }
|
||||
}
|
||||
|
||||
public Color Color
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Vector2 WorldPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector2 worldPos = position
|
||||
+ clippedSourceRect.Size.ToVector2() / 2 * Scale
|
||||
+ hull.Rect.Location.ToVector2();
|
||||
if (hull.Submarine != null) { worldPos += hull.Submarine.DrawPosition; }
|
||||
return worldPos;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 Position
|
||||
{
|
||||
get { return position; }
|
||||
}
|
||||
|
||||
public Vector2 NonClampedPosition
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private readonly HashSet<BackgroundSection> affectedSections;
|
||||
|
||||
private readonly Hull hull;
|
||||
|
||||
public readonly float Scale;
|
||||
|
||||
private Rectangle clippedSourceRect;
|
||||
|
||||
private bool cleaned = false;
|
||||
|
||||
public Decal(DecalPrefab prefab, float scale, Vector2 worldPosition, Hull hull)
|
||||
{
|
||||
Prefab = prefab;
|
||||
|
||||
this.hull = hull;
|
||||
|
||||
//transform to hull-relative coordinates so we don't have to worry about the hull moving
|
||||
NonClampedPosition = position = worldPosition - hull.WorldRect.Location.ToVector2();
|
||||
|
||||
Vector2 drawPos = position + hull.Rect.Location.ToVector2();
|
||||
|
||||
Sprite = prefab.Sprites[Rand.Range(0, prefab.Sprites.Count, Rand.RandSync.Unsynced)];
|
||||
Color = prefab.Color;
|
||||
|
||||
Rectangle drawRect = new Rectangle(
|
||||
(int)(drawPos.X - Sprite.size.X / 2 * scale),
|
||||
(int)(drawPos.Y + Sprite.size.Y / 2 * scale),
|
||||
(int)(Sprite.size.X * scale),
|
||||
(int)(Sprite.size.Y * scale));
|
||||
|
||||
Rectangle overFlowAmount = new Rectangle(
|
||||
(int)Math.Max(hull.Rect.X - drawRect.X, 0.0f),
|
||||
(int)Math.Max(drawRect.Y - hull.Rect.Y, 0.0f),
|
||||
(int)Math.Max(drawRect.Right - hull.Rect.Right, 0.0f),
|
||||
(int)Math.Max((hull.Rect.Y - hull.Rect.Height) - (drawRect.Y - drawRect.Height), 0.0f));
|
||||
|
||||
clippedSourceRect = new Rectangle(
|
||||
Sprite.SourceRect.X + (int)(overFlowAmount.X / scale),
|
||||
Sprite.SourceRect.Y + (int)(overFlowAmount.Y / scale),
|
||||
Sprite.SourceRect.Width - (int)((overFlowAmount.X + overFlowAmount.Width) / scale),
|
||||
Sprite.SourceRect.Height - (int)((overFlowAmount.Y + overFlowAmount.Height) / scale));
|
||||
|
||||
position -= new Vector2(Sprite.size.X / 2 * scale - overFlowAmount.X, -Sprite.size.Y / 2 * scale + overFlowAmount.Y);
|
||||
|
||||
this.Scale = scale;
|
||||
|
||||
foreach (BackgroundSection section in hull.GetBackgroundSectionsViaContaining(new Rectangle((int)position.X, (int)position.Y - drawRect.Height, drawRect.Width, drawRect.Height)))
|
||||
{
|
||||
affectedSections ??= new HashSet<BackgroundSection>();
|
||||
affectedSections.Add(section);
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
fadeTimer += deltaTime;
|
||||
}
|
||||
|
||||
public void ForceRefreshFadeTimer(float val)
|
||||
{
|
||||
cleaned = false;
|
||||
fadeTimer = val;
|
||||
}
|
||||
|
||||
public void StopFadeIn()
|
||||
{
|
||||
Color *= GetAlpha();
|
||||
fadeTimer = Prefab.FadeInTime;
|
||||
}
|
||||
|
||||
public bool AffectsSection(BackgroundSection section)
|
||||
{
|
||||
return affectedSections != null && affectedSections.Contains(section);
|
||||
}
|
||||
|
||||
public void Clean(float val)
|
||||
{
|
||||
cleaned = true;
|
||||
float sizeModifier = MathHelper.Clamp(Sprite.size.X * Sprite.size.Y * Scale / 10000, 1.0f, 25.0f);
|
||||
baseAlpha -= val * -1 / sizeModifier;
|
||||
}
|
||||
|
||||
private float GetAlpha()
|
||||
{
|
||||
if (fadeTimer < Prefab.FadeInTime && !cleaned)
|
||||
{
|
||||
return baseAlpha * fadeTimer / Prefab.FadeInTime;
|
||||
}
|
||||
else if (cleaned || fadeTimer > Prefab.LifeTime - Prefab.FadeOutTime)
|
||||
{
|
||||
return baseAlpha * Math.Min((Prefab.LifeTime - fadeTimer) / Prefab.FadeOutTime, 1.0f);
|
||||
}
|
||||
return baseAlpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class DecalManager
|
||||
{
|
||||
public PrefabCollection<DecalPrefab> Prefabs { get; private set; }
|
||||
|
||||
public readonly List<Sprite> GrimeSprites = new List<Sprite>();
|
||||
private Dictionary<string, List<Sprite>> grimeSpritesByFile = new Dictionary<string, List<Sprite>>();
|
||||
|
||||
public DecalManager()
|
||||
{
|
||||
Prefabs = new PrefabCollection<DecalPrefab>();
|
||||
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Decals))
|
||||
{
|
||||
LoadFromFile(configFile);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadFromFile(ContentFile configFile)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
|
||||
if (doc == null) { return; }
|
||||
|
||||
if (grimeSpritesByFile.ContainsKey(configFile.Path))
|
||||
{
|
||||
foreach (Sprite sprite in grimeSpritesByFile[configFile.Path])
|
||||
{
|
||||
sprite.Remove();
|
||||
GrimeSprites.Remove(sprite);
|
||||
}
|
||||
grimeSpritesByFile.Remove(configFile.Path);
|
||||
}
|
||||
|
||||
bool allowOverriding = false;
|
||||
var mainElement = doc.Root;
|
||||
if (doc.Root.IsOverride())
|
||||
{
|
||||
mainElement = doc.Root.FirstElement();
|
||||
allowOverriding = true;
|
||||
}
|
||||
|
||||
foreach (XElement sourceElement in mainElement.Elements())
|
||||
{
|
||||
var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement;
|
||||
string name = element.Name.ToString().ToLowerInvariant();
|
||||
|
||||
switch (name)
|
||||
{
|
||||
case "grime":
|
||||
if (!grimeSpritesByFile.ContainsKey(configFile.Path))
|
||||
{
|
||||
grimeSpritesByFile.Add(configFile.Path, new List<Sprite>());
|
||||
}
|
||||
var grimeSprite = new Sprite(element);
|
||||
GrimeSprites.Add(grimeSprite);
|
||||
grimeSpritesByFile[configFile.Path].Add(grimeSprite);
|
||||
break;
|
||||
default:
|
||||
if (Prefabs.ContainsKey(name))
|
||||
{
|
||||
if (allowOverriding || sourceElement.IsOverride())
|
||||
{
|
||||
DebugConsole.NewMessage($"Overriding the existing decal prefab '{name}' using the file '{configFile.Path}'", Color.Yellow);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in '{configFile.Path}': Duplicate decal prefab '{name}' found in '{configFile.Path}'! Each decal prefab must have a unique name. " +
|
||||
"Use <override></override> tags to override prefabs.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Prefabs.Add(new DecalPrefab(element, configFile), allowOverriding || sourceElement.IsOverride());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
using MD5 md5 = MD5.Create();
|
||||
foreach (DecalPrefab prefab in Prefabs)
|
||||
{
|
||||
prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5);
|
||||
|
||||
//it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small
|
||||
var collision = Prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier);
|
||||
if (collision != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Hashing collision when generating uint identifiers for Decals: " + prefab.Identifier + " has the same identifier as " + collision.Identifier + " (" + prefab.UIntIdentifier + ")");
|
||||
collision.UIntIdentifier++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveByFile(string filePath)
|
||||
{
|
||||
Prefabs.RemoveByFile(filePath);
|
||||
if (grimeSpritesByFile.ContainsKey(filePath))
|
||||
{
|
||||
foreach (Sprite sprite in grimeSpritesByFile[filePath])
|
||||
{
|
||||
sprite.Remove();
|
||||
GrimeSprites.Remove(sprite);
|
||||
}
|
||||
grimeSpritesByFile.Remove(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull)
|
||||
{
|
||||
if (!Prefabs.ContainsKey(decalName.ToLowerInvariant()))
|
||||
{
|
||||
DebugConsole.ThrowError("Decal prefab " + decalName + " not found!");
|
||||
return null;
|
||||
}
|
||||
|
||||
DecalPrefab prefab = Prefabs[decalName];
|
||||
|
||||
return new Decal(prefab, scale, worldPosition, hull);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class DecalPrefab : IPrefab, IDisposable
|
||||
{
|
||||
public readonly string Name;
|
||||
|
||||
public string OriginalName { get { return Name; } }
|
||||
|
||||
public string Identifier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier that's generated by hashing the prefab's string identifier.
|
||||
/// Used to reduce the amount of bytes needed to write decal data into network messages in multiplayer.
|
||||
/// </summary>
|
||||
public uint UIntIdentifier;
|
||||
|
||||
public string FilePath { get; private set; }
|
||||
|
||||
public ContentPackage ContentPackage { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (Sprite spr in Sprites)
|
||||
{
|
||||
spr.Remove();
|
||||
}
|
||||
Sprites.Clear();
|
||||
}
|
||||
|
||||
public readonly List<Sprite> Sprites;
|
||||
|
||||
public readonly Color Color;
|
||||
|
||||
public readonly float LifeTime;
|
||||
public readonly float FadeOutTime;
|
||||
public readonly float FadeInTime;
|
||||
|
||||
public DecalPrefab(XElement element, ContentFile file)
|
||||
{
|
||||
Name = element.Name.ToString();
|
||||
|
||||
Identifier = Name.ToLowerInvariant();
|
||||
|
||||
FilePath = file.Path;
|
||||
|
||||
ContentPackage = file.ContentPackage;
|
||||
|
||||
Sprites = new List<Sprite>();
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().Equals("sprite", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Sprites.Add(new Sprite(subElement));
|
||||
}
|
||||
}
|
||||
|
||||
Color = new Color(element.GetAttributeVector4("color", Vector4.One));
|
||||
|
||||
LifeTime = element.GetAttributeFloat("lifetime", 10.0f);
|
||||
FadeOutTime = Math.Min(LifeTime, element.GetAttributeFloat("fadeouttime", 1.0f));
|
||||
FadeInTime = Math.Min(LifeTime - FadeOutTime, element.GetAttributeFloat("fadeintime", 0.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
OnEating,
|
||||
OnDeath = OnBroken,
|
||||
OnDamaged,
|
||||
OnSevered
|
||||
OnSevered,
|
||||
OnProduceSpawned
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,11 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
protected bool HasBeenDetermined()
|
||||
{
|
||||
return succeeded.HasValue;
|
||||
}
|
||||
|
||||
public override bool SetGoToTarget(string goTo)
|
||||
{
|
||||
if (Success != null && Success.SetGoToTarget(goTo))
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -34,11 +33,11 @@ namespace Barotrauma
|
||||
if (!(target is Character chr)) { continue; }
|
||||
if (chr.Inventory == null) { continue; }
|
||||
|
||||
if (itemTags.Any(tag => chr.Inventory.Items.Any(item => item != null && item.HasTag(tag)))) { return true; }
|
||||
if (itemTags.Any(tag => chr.Inventory.FindItemByTag(tag, recursive: true) != null)) { return true; }
|
||||
|
||||
foreach (var identifier in itemIdentifierSplit)
|
||||
{
|
||||
if (chr.Inventory.Items.Any(it => it != null && it.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)))
|
||||
if (chr.Inventory.FindItemByIdentifier(identifier, recursive: true) != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -50,15 +49,9 @@ namespace Barotrauma
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
string subActionStr = "";
|
||||
if (succeeded.HasValue)
|
||||
{
|
||||
subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}";
|
||||
}
|
||||
return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
|
||||
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
|
||||
$"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}" +
|
||||
$"Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" +
|
||||
subActionStr;
|
||||
$"Succeeded: {succeeded.ColorizeObject()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class CheckMoneyAction : BinaryOptionAction
|
||||
{
|
||||
[Serialize(0, true)]
|
||||
public int Amount { get; set; }
|
||||
|
||||
public CheckMoneyAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool? DetermineSuccess()
|
||||
{
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
return campaign.Money >= Amount;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
string subActionStr = "";
|
||||
if (succeeded.HasValue)
|
||||
{
|
||||
subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}";
|
||||
}
|
||||
return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckMoneyAction)} -> (Amount: {Amount.ColorizeObject()}" +
|
||||
$" Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" +
|
||||
subActionStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,15 @@ namespace Barotrauma
|
||||
[Serialize(AIObjectiveCombat.CombatMode.Offensive, true)]
|
||||
public AIObjectiveCombat.CombatMode CombatMode { get; set; }
|
||||
|
||||
[Serialize(false, true, description: "Did this NPC start the fight (as an aggressor)?")]
|
||||
public bool IsInstigator { get; set; }
|
||||
|
||||
[Serialize(AIObjectiveCombat.CombatMode.None, true)]
|
||||
public AIObjectiveCombat.CombatMode GuardReaction { get; set; }
|
||||
|
||||
[Serialize(AIObjectiveCombat.CombatMode.None, true)]
|
||||
public AIObjectiveCombat.CombatMode WitnessReaction { get; set; }
|
||||
|
||||
[Serialize("", true)]
|
||||
public string NPCTag { get; set; }
|
||||
|
||||
@@ -50,7 +59,8 @@ namespace Barotrauma
|
||||
}
|
||||
if (enemy == null) { continue; }
|
||||
|
||||
npc.TurnedHostileByEvent = true;
|
||||
npc.CombatAction = this;
|
||||
|
||||
var objectiveManager = humanAiController.ObjectiveManager;
|
||||
foreach (var goToObjective in objectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
|
||||
{
|
||||
|
||||
+22
-24
@@ -55,6 +55,7 @@ namespace Barotrauma
|
||||
private Character speaker;
|
||||
|
||||
private OrderInfo? prevSpeakerOrder;
|
||||
private AIObjective prevIdleObjective, prevGotoObjective;
|
||||
|
||||
public List<SubactionGroup> Options { get; private set; }
|
||||
|
||||
@@ -169,13 +170,19 @@ namespace Barotrauma
|
||||
#if SERVER
|
||||
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
|
||||
#endif
|
||||
if (prevSpeakerOrder != null)
|
||||
var humanAI = speaker.AIController as HumanAIController;
|
||||
if (humanAI != null)
|
||||
{
|
||||
(speaker.AIController as HumanAIController)?.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
(speaker.AIController as HumanAIController)?.SetOrder(null, string.Empty, orderGiver: null, speak: false);
|
||||
if (prevSpeakerOrder != null)
|
||||
{
|
||||
humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false);
|
||||
}
|
||||
if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); }
|
||||
if (prevGotoObjective != null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,9 +253,12 @@ namespace Barotrauma
|
||||
TryStartConversation(null);
|
||||
}
|
||||
}
|
||||
else if (Options.Any())
|
||||
else
|
||||
{
|
||||
Options[selectedOption].Update(deltaTime);
|
||||
if (Options.Any())
|
||||
{
|
||||
Options[selectedOption].Update(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +310,8 @@ namespace Barotrauma
|
||||
{
|
||||
prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption);
|
||||
}
|
||||
prevIdleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
|
||||
prevGotoObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveGoTo>();
|
||||
humanAI.SetOrder(
|
||||
Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)),
|
||||
option: string.Empty, orderGiver: null, speak: false);
|
||||
@@ -334,25 +346,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (!interrupt)
|
||||
{
|
||||
SubactionGroup selOtion = null;
|
||||
if (selectedOption >= 0 && Options.Count > selectedOption)
|
||||
{
|
||||
selOtion = Options[selectedOption];
|
||||
}
|
||||
|
||||
EventAction subAction = null;
|
||||
if (selOtion != null)
|
||||
{
|
||||
subAction = selOtion.CurrentSubAction;
|
||||
}
|
||||
|
||||
return $"{ToolBox.GetDebugSymbol(selectedOption > -1)} {nameof(ConversationAction)} -> (Selected option: {selOtion?.Text.ColorizeObject()})\n" +
|
||||
$" Sub action: {subAction.ColorizeObject()}";
|
||||
return $"{ToolBox.GetDebugSymbol(selectedOption > -1, selectedOption < 0 && dialogOpened)} {nameof(ConversationAction)} -> (Selected option: {selectedOption.ColorizeObject()})";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{ToolBox.GetDebugSymbol(true)} {nameof(ConversationAction)} -> (Interrupted)\n" +
|
||||
$" Sub action: {Interrupted?.CurrentSubAction.ColorizeObject()}";
|
||||
return $"{ToolBox.GetDebugSymbol(true, selectedOption < 0 && dialogOpened)} {nameof(ConversationAction)} -> (Interrupted)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, Amount, target.WorldPosition + Vector2.UnitY * 150.0f);
|
||||
target.Info?.IncreaseSkillLevel(Skill?.ToLowerInvariant(), Amount, target.WorldPosition + Vector2.UnitY * 150.0f);
|
||||
}
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace Barotrauma
|
||||
|
||||
private List<Character> affectedNpcs = null;
|
||||
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
@@ -31,21 +33,18 @@ namespace Barotrauma
|
||||
|
||||
if (Wait)
|
||||
{
|
||||
var newObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true)
|
||||
gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true)
|
||||
{
|
||||
OverridePriority = 100.0f
|
||||
};
|
||||
humanAiController.ObjectiveManager.AddObjective(newObjective);
|
||||
humanAiController.ObjectiveManager.AddObjective(gotoObjective);
|
||||
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
|
||||
if (gotoObjective != null)
|
||||
{
|
||||
if (goToObjective.Target == npc)
|
||||
{
|
||||
goToObjective.Abandon = true;
|
||||
}
|
||||
gotoObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,13 +63,10 @@ namespace Barotrauma
|
||||
foreach (var npc in affectedNpcs)
|
||||
{
|
||||
if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; }
|
||||
foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
|
||||
if (gotoObjective != null)
|
||||
{
|
||||
if (goToObjective.Target == npc)
|
||||
{
|
||||
goToObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
gotoObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
affectedNpcs = null;
|
||||
}
|
||||
|
||||
@@ -11,21 +11,18 @@ namespace Barotrauma
|
||||
|
||||
public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
|
||||
|
||||
private bool isFinished;
|
||||
|
||||
protected override bool? DetermineSuccess()
|
||||
{
|
||||
isFinished = true;
|
||||
return Rand.Range(0.0, 1.0) <= Chance;
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
string subActionStr = "";
|
||||
if (succeeded.HasValue)
|
||||
{
|
||||
subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}";
|
||||
}
|
||||
return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(RNGAction)} -> (Chance: {Chance.ColorizeObject()}, "+
|
||||
$"Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" +
|
||||
subActionStr;
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(RNGAction)} -> (Chance: {Chance.ColorizeObject()}, "+
|
||||
$"Succeeded: {succeeded.ColorizeObject()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -15,7 +16,17 @@ namespace Barotrauma
|
||||
[Serialize(1, true)]
|
||||
public int Amount { get; set; }
|
||||
|
||||
public RemoveItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
|
||||
public RemoveItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ItemIdentifier))
|
||||
{
|
||||
ItemIdentifier = element.GetAttributeString("itemidentifiers", "");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(ItemIdentifier))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - RemoveItemAction without an item identifier.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool isFinished = false;
|
||||
|
||||
@@ -32,25 +43,33 @@ namespace Barotrauma
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
var targets = ParentEvent.GetTargets(TargetTag)
|
||||
.Where(t => t is Character chr && chr.Inventory != null)
|
||||
.Select(t => t as Character).ToList();
|
||||
if (targets.Count <= 0) { return; }
|
||||
|
||||
int count = Amount;
|
||||
while (count > 0 && targets.Count > 0)
|
||||
var targets = ParentEvent.GetTargets(TargetTag);
|
||||
bool hasValidTargets = false;
|
||||
foreach (Entity target in targets)
|
||||
{
|
||||
var items = targets[0].Inventory.Items;
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
if (target is Character character && character.Inventory != null)
|
||||
{
|
||||
if (items[i] != null && items[i].Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
Entity.Spawner.AddToRemoveQueue(items[i]);
|
||||
count--;
|
||||
if (count <= 0) { break; }
|
||||
}
|
||||
hasValidTargets = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasValidTargets) { return; }
|
||||
|
||||
List<Item> usedItems = new List<Item>();
|
||||
foreach (Entity target in targets)
|
||||
{
|
||||
Inventory inventory = (target as Character)?.Inventory;
|
||||
if (inventory == null) { continue; }
|
||||
while (usedItems.Count < Amount)
|
||||
{
|
||||
var item = inventory.FindItem(it =>
|
||||
it != null &&
|
||||
!usedItems.Contains(it) &&
|
||||
it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase), recursive: true);
|
||||
if (item == null) { break; }
|
||||
Entity.Spawner.AddToRemoveQueue(item);
|
||||
usedItems.Add(item);
|
||||
}
|
||||
targets.RemoveAt(0);
|
||||
}
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
@@ -40,38 +40,42 @@ namespace Barotrauma
|
||||
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
object currentValue = campaign.CampaignMetadata.GetValue(Identifier);
|
||||
object xmlValue = ConvertXMLValue();
|
||||
|
||||
float? originalValue = ConvertValueToFloat(currentValue ?? 0);
|
||||
float? newValue = ConvertValueToFloat(xmlValue);
|
||||
|
||||
if ((originalValue == null || newValue == null) && Operation != OperationType.Set)
|
||||
{
|
||||
DebugConsole.ThrowError($"Tried to perform numeric operations to a non number via SetDataAction (Existing: {currentValue?.GetType()}, New: {xmlValue.GetType()})");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Identifier != null)
|
||||
{
|
||||
switch (Operation)
|
||||
{
|
||||
case OperationType.Set:
|
||||
campaign.CampaignMetadata.SetValue(Identifier, xmlValue);
|
||||
break;
|
||||
case OperationType.Add:
|
||||
campaign.CampaignMetadata.SetValue(Identifier, originalValue + newValue ?? 0);
|
||||
break;
|
||||
case OperationType.Multiply:
|
||||
campaign.CampaignMetadata.SetValue(Identifier, originalValue * newValue ?? 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
object xmlValue = ConvertXMLValue(Value);
|
||||
PerformOperation(campaign.CampaignMetadata, Identifier, xmlValue, Operation);
|
||||
}
|
||||
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
public static void PerformOperation(CampaignMetadata metadata, string identifier, object value, OperationType operation)
|
||||
{
|
||||
if (metadata == null) { return; }
|
||||
|
||||
object currentValue = metadata.GetValue(identifier);
|
||||
|
||||
float? originalValue = ConvertValueToFloat(currentValue ?? 0);
|
||||
float? newValue = ConvertValueToFloat(value);
|
||||
|
||||
if ((originalValue == null || newValue == null) && operation != OperationType.Set)
|
||||
{
|
||||
DebugConsole.ThrowError($"Tried to perform numeric operations to a non number via SetDataAction (Existing: {currentValue?.GetType()}, New: {value.GetType()})");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case OperationType.Set:
|
||||
metadata.SetValue(identifier, value);
|
||||
break;
|
||||
case OperationType.Add:
|
||||
metadata.SetValue(identifier, originalValue + newValue ?? 0);
|
||||
break;
|
||||
case OperationType.Multiply:
|
||||
metadata.SetValue(identifier, originalValue * newValue ?? 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static float? ConvertValueToFloat(object value)
|
||||
{
|
||||
if (value is float || value is int)
|
||||
@@ -82,24 +86,24 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
private object ConvertXMLValue()
|
||||
public static object ConvertXMLValue(string value)
|
||||
{
|
||||
if (bool.TryParse(Value, out bool b))
|
||||
if (bool.TryParse(value, out bool b))
|
||||
{
|
||||
return b;
|
||||
}
|
||||
|
||||
if (float.TryParse(Value, out float f))
|
||||
if (float.TryParse(value, out float f))
|
||||
{
|
||||
return f;
|
||||
}
|
||||
|
||||
return Value;
|
||||
return value;
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetDataAction)} -> (Identifier: {Identifier.ColorizeObject()}, Value: {ConvertXMLValue().ColorizeObject()}, Operation: {Operation.ColorizeObject()})";
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetDataAction)} -> (Identifier: {Identifier.ColorizeObject()}, Value: {ConvertXMLValue(Value).ColorizeObject()}, Operation: {Operation.ColorizeObject()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,15 +32,9 @@ namespace Barotrauma
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
string subActionStr = "";
|
||||
if (succeeded.HasValue)
|
||||
{
|
||||
subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}";
|
||||
}
|
||||
return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(SkillCheckAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
|
||||
$"Required skill: {RequiredSkill.ColorizeObject()}, Required level: {RequiredLevel.ColorizeObject()}, " +
|
||||
$"Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" +
|
||||
subActionStr;
|
||||
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(SkillCheckAction)} -> (Target: {TargetTag.ColorizeObject()}, " +
|
||||
$"Skill: {RequiredSkill.ColorizeObject()}, Level: {RequiredLevel.ColorizeObject()}, " +
|
||||
$"Succeeded: {succeeded.ColorizeObject()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,13 +41,18 @@ namespace Barotrauma
|
||||
}
|
||||
public override void Reset()
|
||||
{
|
||||
isRunning = false;
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
public bool isRunning = false;
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
isRunning = true;
|
||||
|
||||
var targets1 = ParentEvent.GetTargets(Target1Tag);
|
||||
if (!targets1.Any()) { return; }
|
||||
|
||||
@@ -155,6 +160,8 @@ namespace Barotrauma
|
||||
{
|
||||
ParentEvent.AddTarget(ApplyToTarget2, entity2);
|
||||
}
|
||||
|
||||
isRunning = false;
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
@@ -162,11 +169,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (string.IsNullOrEmpty(TargetModuleType))
|
||||
{
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TriggerAction)} -> (Distance: {((int)distance).ColorizeObject()}, Radius: {Radius.ColorizeObject()}, TargetTags: {Target1Tag.ColorizeObject()}, {Target2Tag.ColorizeObject()})";
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (Distance: {((int)distance).ColorizeObject()}, Radius: {Radius.ColorizeObject()}, TargetTags: {Target1Tag.ColorizeObject()}, {Target2Tag.ColorizeObject()})";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TriggerAction)} -> (TargetTags: {Target1Tag.ColorizeObject()}, {TargetModuleType.ColorizeObject()})";
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (TargetTags: {Target1Tag.ColorizeObject()}, {TargetModuleType.ColorizeObject()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace Barotrauma
|
||||
|
||||
const float CalculateDistanceTraveledInterval = 5.0f;
|
||||
|
||||
const int MaxEventHistory = 20;
|
||||
|
||||
private Level level;
|
||||
|
||||
private readonly List<Sprite> preloadedSprites = new List<Sprite>();
|
||||
@@ -110,10 +112,25 @@ namespace Barotrauma
|
||||
|
||||
if (level?.LevelData?.Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab));
|
||||
if (level.LevelData.EventHistory.Count > 10)
|
||||
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
|
||||
if (level.LevelData.EventHistory.Count > MaxEventHistory)
|
||||
{
|
||||
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - 10);
|
||||
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
|
||||
}
|
||||
AddChildEvents(initialEventSet);
|
||||
void AddChildEvents(EventSet eventSet)
|
||||
{
|
||||
foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First))
|
||||
{
|
||||
if (!level.LevelData.NonRepeatableEvents.Contains(ep))
|
||||
{
|
||||
level.LevelData.NonRepeatableEvents.Add(ep);
|
||||
}
|
||||
}
|
||||
foreach (EventSet childSet in eventSet.ChildSets)
|
||||
{
|
||||
AddChildEvents(childSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +318,7 @@ namespace Barotrauma
|
||||
|
||||
private float CalculateCommonness(Pair<EventPrefab, float> eventPrefab)
|
||||
{
|
||||
if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.First)) { return 0.0f; }
|
||||
float retVal = eventPrefab.Second;
|
||||
if (level.LevelData.EventHistory.Contains(eventPrefab.First)) { retVal *= 0.1f; }
|
||||
return retVal;
|
||||
@@ -324,7 +342,13 @@ namespace Barotrauma
|
||||
{
|
||||
if (eventSet.EventPrefabs.Count > 0)
|
||||
{
|
||||
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
|
||||
int seed = ToolBox.StringToInt(level.Seed);
|
||||
foreach (var previousEvent in level.LevelData.EventHistory)
|
||||
{
|
||||
seed |= ToolBox.StringToInt(previousEvent.Identifier);
|
||||
}
|
||||
|
||||
MTRandom rand = new MTRandom(seed);
|
||||
List<Pair<EventPrefab, float>> unusedEvents = new List<Pair<EventPrefab, float>>(eventSet.EventPrefabs);
|
||||
for (int j = 0; j < eventSet.EventCount; j++)
|
||||
{
|
||||
@@ -476,37 +500,42 @@ namespace Barotrauma
|
||||
|
||||
eventThreshold += settings.EventThresholdIncrease * deltaTime;
|
||||
eventCoolDown -= deltaTime;
|
||||
|
||||
|
||||
if (currentIntensity < eventThreshold)
|
||||
{
|
||||
//activate pending event sets that can be activated
|
||||
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
|
||||
bool recheck = false;
|
||||
do
|
||||
{
|
||||
var eventSet = pendingEventSets[i];
|
||||
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
|
||||
|
||||
if (!CanStartEventSet(eventSet)) { continue; }
|
||||
|
||||
eventThreshold = settings.DefaultEventThreshold;
|
||||
eventCoolDown = settings.EventCooldown;
|
||||
|
||||
pendingEventSets.RemoveAt(i);
|
||||
|
||||
if (selectedEvents.ContainsKey(eventSet))
|
||||
recheck = false;
|
||||
//activate pending event sets that can be activated
|
||||
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
|
||||
{
|
||||
//start events in this set
|
||||
foreach (Event ev in selectedEvents[eventSet])
|
||||
var eventSet = pendingEventSets[i];
|
||||
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
|
||||
|
||||
if (!CanStartEventSet(eventSet)) { continue; }
|
||||
|
||||
pendingEventSets.RemoveAt(i);
|
||||
|
||||
if (selectedEvents.ContainsKey(eventSet))
|
||||
{
|
||||
activeEvents.Add(ev);
|
||||
//start events in this set
|
||||
foreach (Event ev in selectedEvents[eventSet])
|
||||
{
|
||||
activeEvents.Add(ev);
|
||||
eventThreshold = settings.DefaultEventThreshold;
|
||||
eventCoolDown = settings.EventCooldown;
|
||||
}
|
||||
}
|
||||
|
||||
//add child event sets to pending
|
||||
foreach (EventSet childEventSet in eventSet.ChildSets)
|
||||
{
|
||||
pendingEventSets.Add(childEventSet);
|
||||
recheck = true;
|
||||
}
|
||||
}
|
||||
|
||||
//add child event sets to pending
|
||||
foreach (EventSet childEventSet in eventSet.ChildSets)
|
||||
{
|
||||
pendingEventSets.Add(childEventSet);
|
||||
}
|
||||
}
|
||||
} while (recheck);
|
||||
}
|
||||
|
||||
foreach (Event ev in activeEvents)
|
||||
@@ -568,7 +597,7 @@ namespace Barotrauma
|
||||
{
|
||||
//enemy outside and targeting the sub or something in it
|
||||
//moloch adds 0.24 to enemy danger, a crawler 0.02
|
||||
enemyDanger += enemyAI.CombatStrength / 5000.0f;
|
||||
enemyDanger += enemyAI.CombatStrength / 2000.0f;
|
||||
}
|
||||
}
|
||||
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
|
||||
|
||||
@@ -86,6 +86,8 @@ namespace Barotrauma
|
||||
public readonly bool PerRuin;
|
||||
public readonly bool PerWreck;
|
||||
|
||||
public readonly bool OncePerOutpost;
|
||||
|
||||
public readonly Dictionary<string, float> Commonness;
|
||||
|
||||
//Pair.First: event prefab, Pair.Second: commonness
|
||||
@@ -134,6 +136,7 @@ namespace Barotrauma
|
||||
IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? false);
|
||||
PerRuin = element.GetAttributeBool("perruin", false);
|
||||
PerWreck = element.GetAttributeBool("perwreck", false);
|
||||
OncePerOutpost = element.GetAttributeBool("perwreck", false);
|
||||
|
||||
Commonness[""] = 1.0f;
|
||||
foreach (XElement subElement in element.Elements())
|
||||
|
||||
@@ -131,11 +131,9 @@ namespace Barotrauma
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.AtEndPosition)
|
||||
{
|
||||
int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f);
|
||||
|
||||
if (deliveredItemCount >= requiredDeliveryAmount)
|
||||
{
|
||||
GiveReward();
|
||||
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
@@ -145,6 +143,7 @@ namespace Barotrauma
|
||||
if (!item.Removed) { item.Remove(); }
|
||||
}
|
||||
items.Clear();
|
||||
failed = !completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Barotrauma
|
||||
abstract partial class Mission
|
||||
{
|
||||
public readonly MissionPrefab Prefab;
|
||||
protected bool completed;
|
||||
protected bool completed, failed;
|
||||
protected int state;
|
||||
public int State
|
||||
{
|
||||
@@ -74,7 +74,12 @@ namespace Barotrauma
|
||||
get { return completed; }
|
||||
set { completed = value; }
|
||||
}
|
||||
|
||||
|
||||
public bool Failed
|
||||
{
|
||||
get { return failed; }
|
||||
}
|
||||
|
||||
public virtual bool AllowRespawn
|
||||
{
|
||||
get { return true; }
|
||||
@@ -219,6 +224,14 @@ namespace Barotrauma
|
||||
if (faction != null) { faction.Reputation.Value += reputationReward.Value; }
|
||||
}
|
||||
}
|
||||
|
||||
if (Prefab.DataRewards != null)
|
||||
{
|
||||
foreach (var (identifier, value, operation) in Prefab.DataRewards)
|
||||
{
|
||||
SetDataAction.PerformOperation(campaign.CampaignMetadata, identifier, value, operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@ namespace Barotrauma
|
||||
|
||||
public readonly string AchievementIdentifier;
|
||||
|
||||
public readonly Dictionary<string, float> ReputationRewards = new Dictionary<string, float>();
|
||||
public readonly Dictionary<string, float> ReputationRewards = new Dictionary<string, float>();
|
||||
public readonly List<Tuple<string, object, SetDataAction.OperationType>> DataRewards = new List<Tuple<string, object, SetDataAction.OperationType>>();
|
||||
|
||||
public readonly int Commonness;
|
||||
|
||||
@@ -178,6 +179,23 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "metadata":
|
||||
string identifier = subElement.GetAttributeString("identifier", string.Empty);
|
||||
string stringValue = subElement.GetAttributeString("value", string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(stringValue) && !string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
object value = SetDataAction.ConvertXMLValue(stringValue);
|
||||
SetDataAction.OperationType operation = SetDataAction.OperationType.Set;
|
||||
|
||||
string operatingString = subElement.GetAttributeString("operation", string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(operatingString))
|
||||
{
|
||||
operation = (SetDataAction.OperationType) Enum.Parse(typeof(SetDataAction.OperationType), operatingString);
|
||||
}
|
||||
|
||||
DataRewards.Add(Tuple.Create(identifier, value, operation));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return item.WorldPosition;
|
||||
yield return item.GetRootInventoryOwner()?.WorldPosition ?? item.WorldPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,7 +241,8 @@ namespace Barotrauma
|
||||
|
||||
public override void End()
|
||||
{
|
||||
if (item.CurrentHull?.Submarine == null || (!item.CurrentHull.Submarine.AtEndPosition && !item.CurrentHull.Submarine.AtStartPosition) || item.Removed)
|
||||
var root = item.GetRootContainer() ?? item;
|
||||
if (root.CurrentHull?.Submarine == null || (!root.CurrentHull.Submarine.AtEndPosition && !root.CurrentHull.Submarine.AtStartPosition) || item.Removed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -250,6 +251,7 @@ namespace Barotrauma
|
||||
item = null;
|
||||
GiveReward();
|
||||
completed = true;
|
||||
failed = !completed && state > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,16 +202,40 @@ namespace Barotrauma
|
||||
foreach (var position in availablePositions)
|
||||
{
|
||||
Vector2 pos = position.Position.ToVector2();
|
||||
float dist = Vector2.DistanceSquared(pos, GetReferenceSub().WorldPosition);
|
||||
Submarine refSub = GetReferenceSub();
|
||||
float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition);
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
{
|
||||
if (sub.Info.Type != SubmarineType.Player) { continue; }
|
||||
|
||||
float minDistToSub = GetMinDistanceToSub(sub);
|
||||
if (dist > minDistToSub * minDistToSub && dist < closestDist)
|
||||
if (dist < minDistToSub * minDistToSub) { continue; }
|
||||
|
||||
if (closestDist == float.PositiveInfinity)
|
||||
{
|
||||
closestDist = dist;
|
||||
chosenPosition = position;
|
||||
continue;
|
||||
}
|
||||
|
||||
//chosen position behind the sub -> override with anything that's closer or to the right
|
||||
if (chosenPosition.Position.X < refSub.WorldPosition.X)
|
||||
{
|
||||
if (dist < closestDist || pos.X > refSub.WorldPosition.X)
|
||||
{
|
||||
closestDist = dist;
|
||||
chosenPosition = position;
|
||||
}
|
||||
}
|
||||
//chosen position ahead of the sub -> only override with a position that's also ahead
|
||||
else if (chosenPosition.Position.X > refSub.WorldPosition.X)
|
||||
{
|
||||
if (dist < closestDist && pos.X > refSub.WorldPosition.X)
|
||||
{
|
||||
closestDist = dist;
|
||||
chosenPosition = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//only found a spawnpos that's very far from the sub, pick one that's closer
|
||||
|
||||
@@ -50,6 +50,27 @@ namespace Barotrauma.Extensions
|
||||
}
|
||||
}
|
||||
|
||||
public static T RandomElementByWeight<T>(this IEnumerable<T> source, Func<T, float> weightSelector, Rand.RandSync randSync = Rand.RandSync.Unsynced)
|
||||
{
|
||||
float totalWeight = source.Sum(weightSelector);
|
||||
|
||||
float itemWeightIndex = Rand.Range(0f, 1f, randSync) * totalWeight;
|
||||
float currentWeightIndex = 0;
|
||||
|
||||
foreach (T weightedItem in source)
|
||||
{
|
||||
float weight = weightSelector(weightedItem);
|
||||
currentWeightIndex += weight;
|
||||
|
||||
if (currentWeightIndex >= itemWeightIndex)
|
||||
{
|
||||
return weightedItem;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes an action that modifies the collection on each element (such as removing items from the list).
|
||||
/// Creates a temporary list.
|
||||
|
||||
@@ -63,16 +63,17 @@ namespace Barotrauma
|
||||
GameSettings.SendUserStatistics = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameMain.Config?.SelectedContentPackages.Count > 0)
|
||||
|
||||
var allPackages = GameMain.Config?.AllEnabledPackages.ToList();
|
||||
if (allPackages?.Count > 0)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder("ContentPackage: ");
|
||||
int i = 0;
|
||||
foreach (ContentPackage cp in GameMain.Config.SelectedContentPackages)
|
||||
foreach (ContentPackage cp in allPackages)
|
||||
{
|
||||
string trimmedName = cp.Name.Replace(":", "").Replace(" ", "");
|
||||
sb.Append(trimmedName.Substring(0, Math.Min(32, trimmedName.Length)));
|
||||
if (i < GameMain.Config.SelectedContentPackages.Count - 1) { sb.Append(" "); }
|
||||
if (i < allPackages.Count - 1) { sb.Append(" "); }
|
||||
}
|
||||
GameAnalytics.AddDesignEvent(sb.ToString());
|
||||
}
|
||||
@@ -86,9 +87,9 @@ namespace Barotrauma
|
||||
if (!GameSettings.SendUserStatistics) { return; }
|
||||
if (sentEventIdentifiers.Contains(identifier)) { return; }
|
||||
|
||||
if (GameMain.SelectedPackages != null)
|
||||
if (GameMain.Config.AllEnabledPackages != null)
|
||||
{
|
||||
if (GameMain.VanillaContent == null || GameMain.SelectedPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent))
|
||||
if (GameMain.VanillaContent == null || GameMain.Config.AllEnabledPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent))
|
||||
{
|
||||
message = "[MODDED] " + message;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Barotrauma
|
||||
|
||||
private readonly CampaignMode campaign;
|
||||
|
||||
private Location location => campaign.Map.CurrentLocation;
|
||||
private Location Location => campaign?.Map?.CurrentLocation;
|
||||
|
||||
public Action OnItemsInBuyCrateChanged;
|
||||
public Action OnItemsInSellCrateChanged;
|
||||
@@ -120,7 +120,7 @@ namespace Barotrauma
|
||||
// Exchange money
|
||||
var itemValue = GetBuyValueAtCurrentLocation(item);
|
||||
campaign.Money -= itemValue;
|
||||
campaign.Map.CurrentLocation.StoreCurrentBalance += itemValue;
|
||||
Location.StoreCurrentBalance += itemValue;
|
||||
|
||||
if (removeFromCrate)
|
||||
{
|
||||
@@ -136,11 +136,11 @@ namespace Barotrauma
|
||||
OnPurchasedItemsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public int GetBuyValueAtCurrentLocation(PurchasedItem item) => item?.ItemPrefab != null && campaign?.Map?.CurrentLocation != null ?
|
||||
item.Quantity* campaign.Map.CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab) : 0;
|
||||
public int GetBuyValueAtCurrentLocation(PurchasedItem item) => item?.ItemPrefab != null && Location != null ?
|
||||
item.Quantity * Location.GetAdjustedItemBuyPrice(item.ItemPrefab) : 0;
|
||||
|
||||
public int GetSellValueAtCurrentLocation(ItemPrefab itemPrefab, int quantity = 1) => itemPrefab != null && campaign?.Map?.CurrentLocation != null ?
|
||||
quantity * campaign.Map.CurrentLocation.GetAdjustedItemSellPrice(itemPrefab) : 0;
|
||||
public int GetSellValueAtCurrentLocation(ItemPrefab itemPrefab, int quantity = 1) => itemPrefab != null && Location != null ?
|
||||
quantity * Location.GetAdjustedItemSellPrice(itemPrefab) : 0;
|
||||
|
||||
public void CreatePurchasedItems()
|
||||
{
|
||||
@@ -185,7 +185,7 @@ namespace Barotrauma
|
||||
float floorPos = cargoRoom.Rect.Y - cargoRoom.Rect.Height;
|
||||
|
||||
Vector2 position = new Vector2(
|
||||
Rand.Range(cargoRoom.Rect.X + 20, cargoRoom.Rect.Right - 20),
|
||||
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
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace Barotrauma
|
||||
}
|
||||
#if CLIENT
|
||||
AddCharacterToCrewList(character);
|
||||
DisplayCharacterOrder(character, character.CurrentOrder, character.CurrentOrderOption);
|
||||
AddCurrentOrderIcon(character, character.CurrentOrder, character.CurrentOrderOption);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -193,7 +193,8 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
conversationTimer = Rand.Range(5.0f, 10.0f);
|
||||
//longer delay in multiplayer to prevent the server from triggering NPC conversations while the players are still loading the round
|
||||
conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f);
|
||||
}
|
||||
|
||||
public void FireCharacter(CharacterInfo characterInfo)
|
||||
|
||||
@@ -29,9 +29,17 @@ namespace Barotrauma
|
||||
public float Value
|
||||
{
|
||||
get => Math.Min(MaxReputation, Metadata.GetFloat(metaDataIdentifier, InitialReputation));
|
||||
set => Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation));
|
||||
set
|
||||
{
|
||||
Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation));
|
||||
OnReputationValueChanged?.Invoke();
|
||||
OnAnyReputationValueChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public Action OnReputationValueChanged;
|
||||
public static Action OnAnyReputationValueChanged;
|
||||
|
||||
public Reputation(CampaignMetadata metadata, string identifier, int minReputation, int maxReputation, int initialReputation)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(metadata != null);
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Level.Loaded != null && !Level.Loaded.Generating &&
|
||||
if (Level.Loaded?.EndLocation != null && !Level.Loaded.Generating &&
|
||||
Level.Loaded.Type == LevelData.LevelType.LocationConnection &&
|
||||
GetAvailableTransition(out _, out _) == TransitionType.ProgressToNextEmptyLocation)
|
||||
{
|
||||
@@ -270,7 +270,9 @@ namespace Barotrauma
|
||||
{
|
||||
if (leavingSub.AtEndPosition)
|
||||
{
|
||||
if (Map.EndLocation != null && map.SelectedLocation == Map.EndLocation)
|
||||
if (Map.EndLocation != null &&
|
||||
map.SelectedLocation == Map.EndLocation &&
|
||||
Map.EndLocation.Connections.Any(c => c.LevelData == Level.Loaded.LevelData))
|
||||
{
|
||||
nextLevel = map.StartLocation.LevelData;
|
||||
return TransitionType.End;
|
||||
@@ -463,8 +465,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
foreach (CharacterInfo ci in CrewManager.CharacterInfos)
|
||||
foreach (CharacterInfo ci in CrewManager.CharacterInfos.ToList())
|
||||
{
|
||||
if (ci.CauseOfDeath != null)
|
||||
{
|
||||
CrewManager.RemoveCharacterInfo(ci);
|
||||
}
|
||||
ci?.ResetCurrentOrder();
|
||||
}
|
||||
|
||||
@@ -677,7 +683,7 @@ namespace Barotrauma
|
||||
|
||||
public void OutpostNPCAttacked(Character npc, Character attacker, AttackResult attackResult)
|
||||
{
|
||||
if (npc == null || attacker == null || npc.IsDead || npc.TurnedHostileByEvent) { return; }
|
||||
if (npc == null || attacker == null || npc.IsDead || npc.IsInstigator) { return; }
|
||||
if (npc.TeamID != Character.TeamType.FriendlyNPC) { return; }
|
||||
if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; }
|
||||
Location location = Map?.CurrentLocation;
|
||||
|
||||
@@ -36,6 +36,8 @@ namespace Barotrauma
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public virtual void UpdateWhilePaused(float deltaTime) { }
|
||||
|
||||
public GameModePreset Preset
|
||||
{
|
||||
get { return preset; }
|
||||
|
||||
+11
-4
@@ -178,11 +178,18 @@ namespace Barotrauma
|
||||
|
||||
characterData.Clear();
|
||||
string characterDataPath = GetCharacterDataSavePath();
|
||||
var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath);
|
||||
if (characterDataDoc?.Root == null) return;
|
||||
foreach (XElement subElement in characterDataDoc.Root.Elements())
|
||||
if (!File.Exists(characterDataPath))
|
||||
{
|
||||
characterData.Add(new CharacterCampaignData(subElement));
|
||||
DebugConsole.ThrowError($"Failed to load the character data for the campaign. Could not find the file \"{characterDataPath}\".");
|
||||
}
|
||||
else
|
||||
{
|
||||
var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath);
|
||||
if (characterDataDoc?.Root == null) { return; }
|
||||
foreach (XElement subElement in characterDataDoc.Root.Elements())
|
||||
{
|
||||
characterData.Add(new CharacterCampaignData(subElement));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -149,9 +149,9 @@ namespace Barotrauma
|
||||
GameMode = mpCampaign;
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
mpCampaign.LoadNewLevel();
|
||||
//save to ensure the campaign ID in the save file matches the one that got assigned to this campaign instance
|
||||
SaveUtil.SaveGame(saveFile);
|
||||
mpCampaign.LoadNewLevel();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -350,6 +350,7 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void InitializeLevel(Level level)
|
||||
{
|
||||
//make sure no status effects have been carried on from the next round
|
||||
@@ -436,6 +437,9 @@ namespace Barotrauma
|
||||
Submarine.MainSub.SetPosition(Vector2.Zero);
|
||||
return;
|
||||
}
|
||||
|
||||
var originalSubPos = Submarine.WorldPosition;
|
||||
|
||||
if (level.StartOutpost != null)
|
||||
{
|
||||
//start by placing the sub below the outpost
|
||||
@@ -504,6 +508,18 @@ namespace Barotrauma
|
||||
Submarine.NeutralizeBallast();
|
||||
Submarine.EnableMaintainPosition();
|
||||
}
|
||||
|
||||
// Make sure that linked subs which are NOT docked to the main sub
|
||||
// (but still close enough to NOT be considered as 'left behind')
|
||||
// are also moved to keep their relative position to the main sub
|
||||
var linkedSubs = MapEntity.mapEntityList.FindAll(me => me is LinkedSubmarine);
|
||||
foreach (LinkedSubmarine ls in linkedSubs)
|
||||
{
|
||||
if (ls.Sub == null || ls.Submarine != Submarine) { continue; }
|
||||
if (!ls.LoadSub || ls.Sub.DockedTo.Contains(Submarine)) { continue; }
|
||||
if (Submarine.Info.LeftBehindDockingPortIDs.Contains(ls.OriginalLinkedToID)) { continue; }
|
||||
ls.Sub.SetPosition(ls.Sub.WorldPosition + (Submarine.WorldPosition - originalSubPos));
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
@@ -562,7 +578,7 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
public static bool IsCompatibleWithSelectedContentPackages(IList<string> contentPackagePaths, out string errorMsg)
|
||||
public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackagePaths, out string errorMsg)
|
||||
{
|
||||
errorMsg = "";
|
||||
//no known content packages, must be an older save file
|
||||
@@ -571,13 +587,13 @@ namespace Barotrauma
|
||||
List<string> missingPackages = new List<string>();
|
||||
foreach (string packagePath in contentPackagePaths)
|
||||
{
|
||||
if (!GameMain.Config.SelectedContentPackages.Any(cp => cp.Path == packagePath))
|
||||
if (!GameMain.Config.AllEnabledPackages.Any(cp => cp.Path == packagePath))
|
||||
{
|
||||
missingPackages.Add(packagePath);
|
||||
}
|
||||
}
|
||||
List<string> excessPackages = new List<string>();
|
||||
foreach (ContentPackage cp in GameMain.Config.SelectedContentPackages)
|
||||
foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages)
|
||||
{
|
||||
if (!cp.HasMultiplayerIncompatibleContent) { continue; }
|
||||
if (!contentPackagePaths.Any(p => p == cp.Path))
|
||||
@@ -589,10 +605,10 @@ namespace Barotrauma
|
||||
bool orderMismatch = false;
|
||||
if (missingPackages.Count == 0 && missingPackages.Count == 0)
|
||||
{
|
||||
var selectedPackages = GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList();
|
||||
for (int i = 0; i < contentPackagePaths.Count && i < selectedPackages.Count; i++)
|
||||
var enabledPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList();
|
||||
for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Count; i++)
|
||||
{
|
||||
if (contentPackagePaths[i] != selectedPackages[i].Path)
|
||||
if (contentPackagePaths[i] != enabledPackages[i].Path)
|
||||
{
|
||||
orderMismatch = true;
|
||||
break;
|
||||
@@ -653,7 +669,7 @@ namespace Barotrauma
|
||||
}
|
||||
doc.Root.Add(new XAttribute("mapseed", Map.Seed));
|
||||
doc.Root.Add(new XAttribute("selectedcontentpackages",
|
||||
string.Join("|", GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path))));
|
||||
string.Join("|", GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path))));
|
||||
|
||||
((CampaignMode)GameMode).Save(doc.Root);
|
||||
|
||||
|
||||
@@ -627,6 +627,7 @@ namespace Barotrauma
|
||||
pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, level));
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (isSingleplayer)
|
||||
{
|
||||
SetPendingUpgrades(pendingUpgrades);
|
||||
@@ -635,6 +636,9 @@ namespace Barotrauma
|
||||
{
|
||||
loadedUpgrades = pendingUpgrades;
|
||||
}
|
||||
#else
|
||||
SetPendingUpgrades(pendingUpgrades);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void LogError(string text, Dictionary<string, object?> data, Exception e = null)
|
||||
@@ -669,54 +673,6 @@ namespace Barotrauma
|
||||
return values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the client and the server are agreeing on the upgrade levels, if not something has gone wrong.
|
||||
/// </summary>
|
||||
/// <param name="clientUpgrades"></param>
|
||||
/// <param name="serverUpgrades"></param>
|
||||
public static void CompareUpgrades(Dictionary<string, int> clientUpgrades, Dictionary<string, int> serverUpgrades)
|
||||
{
|
||||
int mismatches = 0;
|
||||
DebugLog("Comparing client upgrades to server upgrades...", Color.Orange);
|
||||
foreach (var (key, value) in clientUpgrades)
|
||||
{
|
||||
if (!serverUpgrades.ContainsKey(key))
|
||||
{
|
||||
DebugLog($"Client has an upgrade the server doesn't! {key} lvl. {value}.", Color.Red);
|
||||
mismatches++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value != serverUpgrades[key])
|
||||
{
|
||||
DebugLog($"Client's upgrade level doesn't match the server's! Client: {key} {value}, Server: {key} {serverUpgrades[key]}.", Color.Red);
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
|
||||
DebugLog("...comparing server upgrades to client upgrades...", Color.Orange);
|
||||
foreach (var (key, value) in serverUpgrades)
|
||||
{
|
||||
if (!clientUpgrades.ContainsKey(key))
|
||||
{
|
||||
DebugLog($"Server has an upgrade the client doesn't! {key} lvl. {value}.", Color.Red);
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
|
||||
if (mismatches == 0)
|
||||
{
|
||||
DebugLog("Everything ok!");
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugLog($"{mismatches} mismatches found! This means that the client and the server are disagreeing on upgrade levels and might cause desync.\n", Color.Red);
|
||||
#if CLIENT
|
||||
DebugConsole.IsOpen = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to sync the pending upgrades list in multiplayer.
|
||||
/// </summary>
|
||||
|
||||
@@ -251,7 +251,24 @@ namespace Barotrauma
|
||||
set { TextManager.Language = value; }
|
||||
}
|
||||
|
||||
public readonly List<ContentPackage> SelectedContentPackages = new List<ContentPackage>();
|
||||
public ContentPackage CurrentCorePackage { get; private set; }
|
||||
private readonly List<ContentPackage> enabledRegularPackages = new List<ContentPackage>();
|
||||
public IReadOnlyList<ContentPackage> EnabledRegularPackages
|
||||
{
|
||||
get { return enabledRegularPackages; }
|
||||
}
|
||||
|
||||
public IEnumerable<ContentPackage> AllEnabledPackages
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return CurrentCorePackage;
|
||||
foreach (var package in EnabledRegularPackages)
|
||||
{
|
||||
yield return package;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ContentPackageSelectionDirtyNotification
|
||||
{
|
||||
@@ -267,6 +284,8 @@ namespace Barotrauma
|
||||
|
||||
public volatile bool SuppressModFolderWatcher;
|
||||
|
||||
public volatile bool WaitingForAutoUpdate;
|
||||
|
||||
#if DEBUG
|
||||
public bool AutomaticQuickStartEnabled { get; set; }
|
||||
public bool AutomaticCampaignLoadEnabled { get; set; }
|
||||
@@ -275,7 +294,7 @@ namespace Barotrauma
|
||||
|
||||
private System.IO.FileSystemWatcher modsFolderWatcher;
|
||||
|
||||
private int ContentFileLoadOrder(ContentFile a)
|
||||
private static int ContentFileLoadOrder(ContentFile a)
|
||||
{
|
||||
switch (a.Type)
|
||||
{
|
||||
@@ -292,62 +311,185 @@ namespace Barotrauma
|
||||
|
||||
public void SelectCorePackage(ContentPackage contentPackage, bool forceReloadAll = false)
|
||||
{
|
||||
if (!contentPackage.IsCorePackage) { return; }
|
||||
if (!contentPackage.ContainsRequiredCorePackageFiles(out _)) { return; }
|
||||
|
||||
ContentPackage otherCorePackage = SelectedContentPackages.Where(cp => cp.CorePackage).First();
|
||||
ContentPackage prevCorePackage = CurrentCorePackage;
|
||||
|
||||
SelectedContentPackages.Remove(otherCorePackage);
|
||||
SelectedContentPackages.Add(contentPackage);
|
||||
CurrentCorePackage = contentPackage;
|
||||
|
||||
ContentPackage.SortContentPackages();
|
||||
if (prevCorePackage != null)
|
||||
{
|
||||
List<ContentFile> filesToRemove = prevCorePackage.Files.Where(f1 => forceReloadAll ||
|
||||
!contentPackage.Files.Any(f2 =>
|
||||
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
|
||||
|
||||
List<ContentFile> filesToRemove = otherCorePackage.Files.Where(f1 => forceReloadAll ||
|
||||
!contentPackage.Files.Any(f2 =>
|
||||
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
|
||||
List<ContentFile> filesToAdd = contentPackage.Files.Where(f1 => forceReloadAll ||
|
||||
!prevCorePackage.Files.Any(f2 =>
|
||||
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
|
||||
|
||||
List<ContentFile> filesToAdd = contentPackage.Files.Where(f1 => forceReloadAll ||
|
||||
!otherCorePackage.Files.Any(f2 =>
|
||||
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
|
||||
DisableContentPackageItems(filesToRemove);
|
||||
EnableContentPackageItems(filesToAdd);
|
||||
|
||||
DisableContentPackageItems(filesToRemove.OrderBy(ContentFileLoadOrder));
|
||||
EnableContentPackageItems(filesToAdd.OrderBy(ContentFileLoadOrder));
|
||||
|
||||
RefreshContentPackageItems(filesToAdd.Concat(filesToRemove));
|
||||
RefreshContentPackageItems(filesToAdd.Concat(filesToRemove));
|
||||
}
|
||||
else
|
||||
{
|
||||
EnableContentPackageItems(contentPackage.Files);
|
||||
RefreshContentPackageItems(contentPackage.Files);
|
||||
}
|
||||
}
|
||||
|
||||
public void AutoSelectCorePackage(IEnumerable<ContentPackage> toRemove)
|
||||
{
|
||||
SelectCorePackage(ContentPackage.List.Find(cpp =>
|
||||
cpp.CorePackage &&
|
||||
!toRemove.Contains(cpp) &&
|
||||
SelectCorePackage(ContentPackage.CorePackages.Find(cpp =>
|
||||
(toRemove == null || !toRemove.Contains(cpp)) &&
|
||||
cpp.ContainsRequiredCorePackageFiles(out _)));
|
||||
}
|
||||
|
||||
public void SelectContentPackage(ContentPackage contentPackage)
|
||||
{
|
||||
if (!SelectedContentPackages.Contains(contentPackage))
|
||||
{
|
||||
SelectedContentPackages.Add(contentPackage);
|
||||
ContentPackage.SortContentPackages();
|
||||
private List<Tuple<ContentPackage, bool>> backupModOrder;
|
||||
|
||||
EnableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder));
|
||||
public void SwapPackages(ContentPackage corePackage, List<ContentPackage> regularPackages)
|
||||
{
|
||||
backupModOrder = new List<Tuple<ContentPackage, bool>>();
|
||||
backupModOrder.Add(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)));
|
||||
}
|
||||
|
||||
List<ContentPackage> packagesToDisable = new List<ContentPackage>();
|
||||
packagesToDisable.Add(CurrentCorePackage);
|
||||
packagesToDisable.AddRange(EnabledRegularPackages.Where(p => p.HasMultiplayerIncompatibleContent));
|
||||
List<ContentPackage> packagesToEnable = new List<ContentPackage>();
|
||||
packagesToEnable.Add(corePackage);
|
||||
packagesToEnable.AddRange(regularPackages);
|
||||
|
||||
IEnumerable<ContentFile> filesOfDisabledPkgs = packagesToDisable.SelectMany(p => p.Files);
|
||||
IEnumerable<ContentFile> filesOfEnabledPkgs = packagesToEnable.SelectMany(p => p.Files);
|
||||
|
||||
List<ContentFile> filesToDisable = filesOfDisabledPkgs.Where(f1 =>
|
||||
!filesOfEnabledPkgs.Any(f2 =>
|
||||
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
|
||||
|
||||
List<ContentFile> filesToEnable = filesOfEnabledPkgs.Where(f1 =>
|
||||
!filesOfDisabledPkgs.Any(f2 =>
|
||||
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
|
||||
|
||||
CurrentCorePackage = corePackage;
|
||||
enabledRegularPackages.RemoveAll(p => p.HasMultiplayerIncompatibleContent); enabledRegularPackages.AddRange(regularPackages);
|
||||
|
||||
DisableContentPackageItems(filesToDisable);
|
||||
EnableContentPackageItems(filesToEnable);
|
||||
|
||||
RefreshContentPackageItems(filesOfEnabledPkgs.Concat(filesToDisable));
|
||||
|
||||
ContentPackage.SortContentPackages(p => -regularPackages.IndexOf(p));
|
||||
}
|
||||
|
||||
public void RestoreBackupPackages()
|
||||
{
|
||||
if (backupModOrder == null) { return; }
|
||||
|
||||
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));
|
||||
|
||||
backupModOrder = null;
|
||||
}
|
||||
|
||||
public void EnableRegularPackage(ContentPackage contentPackage)
|
||||
{
|
||||
if (contentPackage.IsCorePackage) { return; }
|
||||
if (!enabledRegularPackages.Contains(contentPackage))
|
||||
{
|
||||
enabledRegularPackages.Add(contentPackage);
|
||||
SortContentPackages();
|
||||
|
||||
EnableContentPackageItems(contentPackage.Files);
|
||||
RefreshContentPackageItems(contentPackage.Files);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeselectContentPackage(ContentPackage contentPackage)
|
||||
public void DisableRegularPackage(ContentPackage contentPackage)
|
||||
{
|
||||
if (SelectedContentPackages.Contains(contentPackage))
|
||||
if (contentPackage.IsCorePackage) { return; }
|
||||
if (enabledRegularPackages.Contains(contentPackage))
|
||||
{
|
||||
SelectedContentPackages.Remove(contentPackage);
|
||||
ContentPackage.SortContentPackages();
|
||||
DisableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder));
|
||||
enabledRegularPackages.Remove(contentPackage);
|
||||
SortContentPackages();
|
||||
|
||||
DisableContentPackageItems(contentPackage.Files);
|
||||
RefreshContentPackageItems(contentPackage.Files);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnableContentPackageItems(IOrderedEnumerable<ContentFile> files)
|
||||
public void SortContentPackages(bool refreshAll = false)
|
||||
{
|
||||
for (int i = enabledRegularPackages.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var package = enabledRegularPackages[i];
|
||||
if (!ContentPackage.RegularPackages.Contains(package))
|
||||
{
|
||||
ContentPackage replacement = ContentPackage.RegularPackages.Find(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase));
|
||||
if (replacement != null)
|
||||
{
|
||||
enabledRegularPackages[i] = replacement;
|
||||
}
|
||||
else
|
||||
{
|
||||
DisableRegularPackage(package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (CurrentCorePackage == null)
|
||||
{
|
||||
AutoSelectCorePackage(null);
|
||||
}
|
||||
else if (!ContentPackage.CorePackages.Contains(CurrentCorePackage))
|
||||
{
|
||||
ContentPackage replacement = ContentPackage.CorePackages.Find(p => p.Name.Equals(CurrentCorePackage.Name, StringComparison.OrdinalIgnoreCase));
|
||||
if (replacement != null)
|
||||
{
|
||||
SelectCorePackage(replacement);
|
||||
}
|
||||
else
|
||||
{
|
||||
AutoSelectCorePackage(null);
|
||||
}
|
||||
}
|
||||
|
||||
var sortedSelected = enabledRegularPackages
|
||||
.OrderBy(p => -ContentPackage.RegularPackages.IndexOf(p))
|
||||
.ToList();
|
||||
enabledRegularPackages.Clear(); enabledRegularPackages.AddRange(sortedSelected);
|
||||
|
||||
CharacterPrefab.Prefabs.SortAll();
|
||||
AfflictionPrefab.Prefabs.SortAll();
|
||||
JobPrefab.Prefabs.SortAll();
|
||||
ItemPrefab.Prefabs.SortAll();
|
||||
CoreEntityPrefab.Prefabs.SortAll();
|
||||
ItemAssemblyPrefab.Prefabs.SortAll();
|
||||
StructurePrefab.Prefabs.SortAll();
|
||||
|
||||
#if CLIENT
|
||||
GameMain.DecalManager?.Prefabs.SortAll();
|
||||
GameMain.ParticleManager?.Prefabs.SortAll();
|
||||
#endif
|
||||
|
||||
if (refreshAll)
|
||||
{
|
||||
RefreshContentPackageItems(AllEnabledPackages.SelectMany(p => p.Files));
|
||||
}
|
||||
}
|
||||
|
||||
public void EnableContentPackageItems(IEnumerable<ContentFile> unorderedFiles)
|
||||
{
|
||||
if (WaitingForAutoUpdate) { return; }
|
||||
IOrderedEnumerable<ContentFile> files = unorderedFiles.OrderBy(ContentFileLoadOrder);
|
||||
foreach (ContentFile file in files)
|
||||
{
|
||||
switch (file.Type)
|
||||
@@ -390,8 +532,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableContentPackageItems(IOrderedEnumerable<ContentFile> files)
|
||||
public void DisableContentPackageItems(IEnumerable<ContentFile> unorderedFiles)
|
||||
{
|
||||
if (WaitingForAutoUpdate) { return; }
|
||||
IOrderedEnumerable<ContentFile> files = unorderedFiles.OrderBy(ContentFileLoadOrder);
|
||||
foreach (ContentFile file in files)
|
||||
{
|
||||
switch (file.Type)
|
||||
@@ -434,9 +578,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshContentPackageItems(IEnumerable<ContentFile> files)
|
||||
public void RefreshContentPackageItems(IEnumerable<ContentFile> files)
|
||||
{
|
||||
if (files.Any(f => f.Type == ContentType.LocationTypes)) { LocationType.Init(); }
|
||||
if (WaitingForAutoUpdate) { return; }
|
||||
if (files.Any(f => f.Type == ContentType.Afflictions)) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); }
|
||||
if (files.Any(f => f.Type == ContentType.Submarine ||
|
||||
f.Type == ContentType.Outpost ||
|
||||
@@ -447,7 +591,13 @@ namespace Barotrauma
|
||||
if (files.Any(f => f.Type == ContentType.Factions)) { FactionPrefab.LoadFactions(); }
|
||||
if (files.Any(f => f.Type == ContentType.Item)) { ItemPrefab.InitFabricationRecipes(); }
|
||||
if (files.Any(f => f.Type == ContentType.RuinConfig)) { RuinGeneration.RuinGenerationParams.ClearAll(); }
|
||||
if (files.Any(f => f.Type == ContentType.RandomEvents)) { EventSet.LoadPrefabs(); }
|
||||
if (files.Any(f => f.Type == ContentType.RandomEvents ||
|
||||
f.Type == ContentType.LocationTypes))
|
||||
{
|
||||
LocationType.List.Clear();
|
||||
EventSet.LoadPrefabs();
|
||||
LocationType.Init();
|
||||
}
|
||||
if (files.Any(f => f.Type == ContentType.Missions)) { MissionPrefab.Init(); }
|
||||
if (files.Any(f => f.Type == ContentType.LevelObjectPrefabs)) { LevelObjectPrefab.LoadAll(); }
|
||||
if (files.Any(f => f.Type == ContentType.MapGenerationParameters)) { MapGenerationParams.Init(); }
|
||||
@@ -515,49 +665,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void ReorderSelectedContentPackages<T>(Func<ContentPackage, T> orderFunction)
|
||||
{
|
||||
ContentPackage.List = ContentPackage.List
|
||||
.OrderByDescending(p => p.CorePackage)
|
||||
.ThenBy(orderFunction)
|
||||
.ToList();
|
||||
|
||||
ContentPackage.SortContentPackages();
|
||||
|
||||
CharacterPrefab.Prefabs.SortAll();
|
||||
AfflictionPrefab.Prefabs.SortAll();
|
||||
JobPrefab.Prefabs.SortAll();
|
||||
ItemPrefab.Prefabs.SortAll();
|
||||
CoreEntityPrefab.Prefabs.SortAll();
|
||||
ItemAssemblyPrefab.Prefabs.SortAll();
|
||||
StructurePrefab.Prefabs.SortAll();
|
||||
|
||||
SubmarineInfo.RefreshSavedSubs();
|
||||
ItemPrefab.InitFabricationRecipes();
|
||||
RuinGeneration.RuinGenerationParams.ClearAll();
|
||||
EventSet.LoadPrefabs();
|
||||
MissionPrefab.Init();
|
||||
LevelObjectPrefab.LoadAll();
|
||||
LocationType.Init();
|
||||
MapGenerationParams.Init();
|
||||
LevelGenerationParams.LoadPresets();
|
||||
OutpostGenerationParams.LoadPresets();
|
||||
TraitorMissionPrefab.Init();
|
||||
Order.Init();
|
||||
EventManagerSettings.Init();
|
||||
WreckAIConfig.LoadAll();
|
||||
SkillSettings.Load(GameMain.Instance.GetFilesOfType(ContentType.SkillSettings));
|
||||
|
||||
#if CLIENT
|
||||
GameMain.DecalManager.Prefabs.SortAll();
|
||||
GameMain.ParticleManager.Prefabs.SortAll();
|
||||
SoundPlayer.Init().ForEach(_ => { return; });
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
private HashSet<string> selectedContentPackagePaths = new HashSet<string>();
|
||||
|
||||
public string MasterServerUrl { get; set; }
|
||||
public string RemoteContentUrl { get; set; }
|
||||
public bool AutoCheckUpdates { get; set; }
|
||||
@@ -621,6 +728,7 @@ namespace Barotrauma
|
||||
private bool showTutorialSkipWarning = true;
|
||||
|
||||
public static bool EnableSubmarineAutoSave { get; set; }
|
||||
public static int MaximumAutoSaves { get; set; }
|
||||
public static Color SubEditorBackgroundColor { get; set; }
|
||||
|
||||
public bool ShowTutorialSkipWarning
|
||||
@@ -662,54 +770,64 @@ namespace Barotrauma
|
||||
case System.IO.WatcherChangeTypes.Created:
|
||||
{
|
||||
string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
|
||||
if (File.Exists(cpPath) && !ContentPackage.List.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath))
|
||||
if (File.Exists(cpPath) &&
|
||||
!ContentPackage.AllPackages.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath))
|
||||
{
|
||||
var cp = new ContentPackage(cpPath);
|
||||
ContentPackage.List.Add(cp);
|
||||
ContentPackage.AddPackage(new ContentPackage(cpPath));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case System.IO.WatcherChangeTypes.Deleted:
|
||||
{
|
||||
string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
|
||||
var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList();
|
||||
var packagesToDeselect = GameMain.Config.SelectedContentPackages.Where(p => toRemove.Contains(p)).ToList();
|
||||
foreach (var cp in packagesToDeselect)
|
||||
{
|
||||
if (cp.CorePackage)
|
||||
{
|
||||
GameMain.Config.AutoSelectCorePackage(toRemove);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.Config.DeselectContentPackage(cp);
|
||||
}
|
||||
}
|
||||
|
||||
var toRemove = ContentPackage.RegularPackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList();
|
||||
foreach (var cp in toRemove)
|
||||
{
|
||||
ContentPackage.List.Remove(cp);
|
||||
if (enabledRegularPackages.Contains(cp)) { DisableRegularPackage(cp); }
|
||||
}
|
||||
|
||||
toRemove.AddRange(ContentPackage.CorePackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath));
|
||||
bool reselectCore = false;
|
||||
foreach (var cp in toRemove)
|
||||
{
|
||||
ContentPackage.RemovePackage(cp);
|
||||
if (cp.IsCorePackage)
|
||||
{
|
||||
reselectCore = true;
|
||||
}
|
||||
}
|
||||
if (reselectCore) { AutoSelectCorePackage(null); }
|
||||
}
|
||||
break;
|
||||
case System.IO.WatcherChangeTypes.Renamed:
|
||||
{
|
||||
System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs;
|
||||
|
||||
string cpPath = Path.GetFullPath(Path.Combine(renameArgs.OldFullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
|
||||
var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList();
|
||||
string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
|
||||
var toRemove = ContentPackage.RegularPackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList();
|
||||
foreach (var cp in toRemove)
|
||||
{
|
||||
GameMain.Config.DeselectContentPackage(cp);
|
||||
ContentPackage.List.Remove(cp);
|
||||
if (enabledRegularPackages.Contains(cp)) { DisableRegularPackage(cp); }
|
||||
}
|
||||
|
||||
toRemove.AddRange(ContentPackage.CorePackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath));
|
||||
bool reselectCore = false;
|
||||
foreach (var cp in toRemove)
|
||||
{
|
||||
ContentPackage.RemovePackage(cp);
|
||||
if (cp.IsCorePackage)
|
||||
{
|
||||
reselectCore = true;
|
||||
}
|
||||
}
|
||||
|
||||
cpPath = Path.GetFullPath(Path.Combine(renameArgs.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
|
||||
if (File.Exists(cpPath) && !ContentPackage.List.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath))
|
||||
if (File.Exists(cpPath) &&
|
||||
!ContentPackage.AllPackages.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath))
|
||||
{
|
||||
var cp = new ContentPackage(cpPath);
|
||||
ContentPackage.List.Add(cp);
|
||||
ContentPackage.AddPackage(new ContentPackage(cpPath));
|
||||
}
|
||||
if (reselectCore) { AutoSelectCorePackage(null); }
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -723,7 +841,7 @@ namespace Barotrauma
|
||||
GraphicsWidth = 1024;
|
||||
GraphicsHeight = 768;
|
||||
MasterServerUrl = "";
|
||||
SelectContentPackage(ContentPackage.List.Any() ? ContentPackage.List[0] : new ContentPackage(""));
|
||||
SelectCorePackage(ContentPackage.CorePackages.FirstOrDefault());
|
||||
jobPreferences = new List<Pair<string, int>>();
|
||||
return;
|
||||
}
|
||||
@@ -778,6 +896,7 @@ namespace Barotrauma
|
||||
new XAttribute("verboselogging", VerboseLogging),
|
||||
new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs),
|
||||
new XAttribute("submarineautosave", EnableSubmarineAutoSave),
|
||||
new XAttribute("maxautosaves", MaximumAutoSaves),
|
||||
new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)),
|
||||
new XAttribute("enablesplashscreen", EnableSplashScreen),
|
||||
new XAttribute("usesteammatchmaking", UseSteamMatchmaking),
|
||||
@@ -831,11 +950,11 @@ namespace Barotrauma
|
||||
new XAttribute("hudscale", HUDScale),
|
||||
new XAttribute("inventoryscale", InventoryScale));
|
||||
|
||||
foreach (ContentPackage contentPackage in SelectedContentPackages)
|
||||
foreach (ContentPackage contentPackage in ContentPackage.CorePackages)
|
||||
{
|
||||
if (contentPackage.Path.Contains(VanillaContentPackagePath))
|
||||
{
|
||||
doc.Root.Add(new XElement("contentpackage", new XAttribute("path", contentPackage.Path)));
|
||||
doc.Root.Add(new XElement("contentpackages", new XElement("core", new XAttribute("name", contentPackage.Name))));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -978,112 +1097,6 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ReloadContentPackages()
|
||||
{
|
||||
LoadContentPackages(selectedContentPackagePaths);
|
||||
}
|
||||
|
||||
private void LoadContentPackages(IEnumerable<string> contentPackagePaths)
|
||||
{
|
||||
var missingPackagePaths = new List<string>();
|
||||
var incompatiblePackages = new List<ContentPackage>();
|
||||
var packagesWithErrors = new List<ContentPackage>();
|
||||
SelectedContentPackages.Clear();
|
||||
foreach (string path in contentPackagePaths)
|
||||
{
|
||||
var matchingContentPackage = ContentPackage.List.Find(cp => Barotrauma.IO.Path.GetFullPath(cp.Path).CleanUpPath() == path.CleanUpPath());
|
||||
|
||||
if (matchingContentPackage == null)
|
||||
{
|
||||
missingPackagePaths.Add(path);
|
||||
}
|
||||
else if (!matchingContentPackage.IsCompatible())
|
||||
{
|
||||
DebugConsole.NewMessage(
|
||||
$"Content package \"{matchingContentPackage.Name}\" is not compatible with this version of Barotrauma (game version: {GameMain.Version}, content package version: {matchingContentPackage.GameVersion})",
|
||||
Color.Red);
|
||||
incompatiblePackages.Add(matchingContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!matchingContentPackage.CheckErrors(out List<string> errorMessages))
|
||||
{
|
||||
DebugConsole.NewMessage(
|
||||
$"Errors found in content package \"{matchingContentPackage.Name}\": " + string.Join(", ", errorMessages),
|
||||
Color.Red);
|
||||
packagesWithErrors.Add(matchingContentPackage);
|
||||
}
|
||||
//add content packages with errors as they are generally able to load most of their assets
|
||||
SelectedContentPackages.Add(matchingContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
EnsureCoreContentPackageSelected(gameLoaded: false);
|
||||
|
||||
ContentPackage.SortContentPackages();
|
||||
TextManager.LoadTextPacks(SelectedContentPackages);
|
||||
|
||||
foreach (ContentPackage contentPackage in SelectedContentPackages)
|
||||
{
|
||||
foreach (ContentFile file in contentPackage.Files)
|
||||
{
|
||||
ToolBox.IsProperFilenameCase(file.Path);
|
||||
}
|
||||
}
|
||||
|
||||
//save to get rid of the invalid selected packages in the config file
|
||||
if (missingPackagePaths.Count > 0 || incompatiblePackages.Count > 0 || packagesWithErrors.Count > 0) { SaveNewPlayerConfig(); }
|
||||
|
||||
//display error messages after all content packages have been loaded
|
||||
//to make sure the package that contains text files has been loaded before we attempt to use TextManager
|
||||
foreach (string missingPackagePath in missingPackagePaths)
|
||||
{
|
||||
DebugConsole.ThrowError(TextManager.GetWithVariable("ContentPackageNotFound", "[packagepath]", missingPackagePath));
|
||||
}
|
||||
foreach (ContentPackage invalidPackage in packagesWithErrors)
|
||||
{
|
||||
DebugConsole.ThrowError(TextManager.GetWithVariable("ContentPackageHasErrors", "[packagename]", invalidPackage.Name), createMessageBox: true);
|
||||
}
|
||||
foreach (ContentPackage incompatiblePackage in incompatiblePackages)
|
||||
{
|
||||
DebugConsole.ThrowError(TextManager.GetWithVariables(incompatiblePackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage",
|
||||
new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { incompatiblePackage.Name, incompatiblePackage.GameVersion.ToString(), GameMain.Version.ToString() }),
|
||||
createMessageBox: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void EnsureCoreContentPackageSelected(bool gameLoaded=true)
|
||||
{
|
||||
if (SelectedContentPackages.Any(cp => cp.CorePackage)) { return; }
|
||||
|
||||
if (GameMain.VanillaContent != null)
|
||||
{
|
||||
if (gameLoaded)
|
||||
{
|
||||
SelectContentPackage(GameMain.VanillaContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedContentPackages.Add(GameMain.VanillaContent);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var availablePackage = ContentPackage.List.FirstOrDefault(cp => cp.IsCompatible() && cp.CorePackage);
|
||||
if (availablePackage != null)
|
||||
{
|
||||
if (gameLoaded)
|
||||
{
|
||||
SelectContentPackage(availablePackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedContentPackages.Add(availablePackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Save PlayerConfig
|
||||
@@ -1106,6 +1119,7 @@ namespace Barotrauma
|
||||
new XAttribute("verboselogging", VerboseLogging),
|
||||
new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs),
|
||||
new XAttribute("submarineautosave", EnableSubmarineAutoSave),
|
||||
new XAttribute("maxautosaves", MaximumAutoSaves),
|
||||
new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)),
|
||||
new XAttribute("enablesplashscreen", EnableSplashScreen),
|
||||
new XAttribute("usesteammatchmaking", UseSteamMatchmaking),
|
||||
@@ -1202,11 +1216,32 @@ namespace Barotrauma
|
||||
new XAttribute("hudscale", HUDScale),
|
||||
new XAttribute("inventoryscale", InventoryScale));
|
||||
|
||||
foreach (ContentPackage contentPackage in SelectedContentPackages)
|
||||
XElement contentPackagesElement = new XElement("contentpackages");
|
||||
|
||||
string corePackageName = (CurrentCorePackage ?? ContentPackage.CorePackages.FirstOrDefault()).Name;
|
||||
contentPackagesElement.Add(new XElement("core", new XAttribute("name", corePackageName)));
|
||||
|
||||
XElement regularPackagesElement = new XElement("regular");
|
||||
foreach (ContentPackage package in ContentPackage.RegularPackages)
|
||||
{
|
||||
doc.Root.Add(new XElement("contentpackage",
|
||||
new XAttribute("path", contentPackage.Path)));
|
||||
XElement packageElement = new XElement("package", new XAttribute("name", package.Name));
|
||||
if (EnabledRegularPackages.Contains(package)) { packageElement.Add(new XAttribute("enabled", "true")); }
|
||||
regularPackagesElement.Add(packageElement);
|
||||
}
|
||||
contentPackagesElement.Add(regularPackagesElement);
|
||||
|
||||
doc.Root.Add(contentPackagesElement);
|
||||
|
||||
#if UNSTABLE
|
||||
//TODO: remove at some point
|
||||
foreach (ContentPackage package in AllEnabledPackages)
|
||||
{
|
||||
XElement compatibilityElement = new XElement("contentpackage");
|
||||
compatibilityElement.Add(new XAttribute("path", package.Path));
|
||||
|
||||
doc.Root.Add(compatibilityElement);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if CLIENT
|
||||
var keyMappingElement = new XElement("keymapping");
|
||||
@@ -1318,6 +1353,7 @@ namespace Barotrauma
|
||||
sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", sendUserStatistics);
|
||||
QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName);
|
||||
EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true);
|
||||
MaximumAutoSaves = doc.Root.GetAttributeInt("maxautosaves", 8);
|
||||
SubEditorBackgroundColor = doc.Root.GetAttributeColor("subeditorbackground", new Color(0.051f, 0.149f, 0.271f, 1.0f));
|
||||
UseSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", UseSteamMatchmaking);
|
||||
RequireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", RequireSteamAuthentication);
|
||||
@@ -1441,18 +1477,79 @@ namespace Barotrauma
|
||||
|
||||
private void LoadContentPackages(XDocument doc)
|
||||
{
|
||||
selectedContentPackagePaths = new HashSet<string>();
|
||||
foreach (XElement subElement in doc.Root.Elements())
|
||||
CurrentCorePackage = null;
|
||||
enabledRegularPackages.Clear();
|
||||
|
||||
var contentPackagesElement = doc.Root.Element("contentpackages");
|
||||
if (contentPackagesElement != null)
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
string coreName = contentPackagesElement.Element("core")?.GetAttributeString("name", "");
|
||||
ContentPackage corePackage = ContentPackage.CorePackages.Find(p => p.Name.Equals(coreName, StringComparison.OrdinalIgnoreCase));
|
||||
if (corePackage != null)
|
||||
{
|
||||
case "contentpackage":
|
||||
string path = Path.GetFullPath(subElement.GetAttributeString("path", ""));
|
||||
selectedContentPackagePaths.Add(path);
|
||||
break;
|
||||
CurrentCorePackage = corePackage;
|
||||
}
|
||||
|
||||
XElement regularElement = contentPackagesElement.Element("regular");
|
||||
|
||||
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; }
|
||||
|
||||
string name = subElement.GetAttributeString("name", null);
|
||||
if (string.IsNullOrEmpty(name)) { continue; }
|
||||
|
||||
var package = ContentPackage.RegularPackages.Find(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
if (package == null) { continue; }
|
||||
enabledRegularPackages.Add(package);
|
||||
}
|
||||
}
|
||||
}
|
||||
LoadContentPackages(selectedContentPackagePaths);
|
||||
else
|
||||
{
|
||||
var enabledContentPackagePaths = new List<string>();
|
||||
foreach (XElement subElement in doc.Root.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "contentpackage":
|
||||
string path = subElement.GetAttributeString("path", "");
|
||||
enabledContentPackagePaths.Add(path.CleanUpPath().ToLowerInvariant());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ContentPackage.SortContentPackages(p => enabledContentPackagePaths.IndexOf(p.Path.CleanUpPath().ToLowerInvariant()));
|
||||
|
||||
foreach (string path in enabledContentPackagePaths)
|
||||
{
|
||||
ContentPackage package = ContentPackage.AllPackages
|
||||
.FirstOrDefault(p => p.Path.CleanUpPath().Equals(path, StringComparison.OrdinalIgnoreCase));
|
||||
if (package == null) { continue; }
|
||||
if (package.IsCorePackage) { CurrentCorePackage = package; }
|
||||
else { enabledRegularPackages.Add(package); }
|
||||
}
|
||||
}
|
||||
|
||||
if (CurrentCorePackage == null)
|
||||
{
|
||||
CurrentCorePackage = ContentPackage.CorePackages.First();
|
||||
}
|
||||
|
||||
TextManager.LoadTextPacks(AllEnabledPackages);
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -18,5 +18,10 @@ namespace Barotrauma
|
||||
Shoot,
|
||||
Command,
|
||||
ToggleInventory
|
||||
#if DEBUG
|
||||
,
|
||||
NextFireMode,
|
||||
PreviousFireMode
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,15 +209,17 @@ namespace Barotrauma.Items.Components
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
|
||||
{
|
||||
originalDockingTargetID = DockingTarget.item.ID;
|
||||
item.CreateServerEvent(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
public void Lock(bool isNetworkMessage, bool forcePosition = false)
|
||||
{
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null && !isNetworkMessage) return;
|
||||
if (GameMain.Client != null && !isNetworkMessage) { return; }
|
||||
#endif
|
||||
|
||||
if (DockingTarget == null)
|
||||
@@ -251,6 +253,7 @@ namespace Barotrauma.Items.Components
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
|
||||
{
|
||||
originalDockingTargetID = DockingTarget.item.ID;
|
||||
item.CreateServerEvent(this);
|
||||
}
|
||||
#else
|
||||
@@ -332,20 +335,45 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (DockingDir != 0) { return DockingDir; }
|
||||
|
||||
if (Door != null)
|
||||
if (Door != null && Door.LinkedGap.linkedTo.Count > 0)
|
||||
{
|
||||
if (Door.LinkedGap.linkedTo.Count == 1)
|
||||
Hull refHull = null;
|
||||
float largestHullSize = 0.0f;
|
||||
foreach (MapEntity linked in Door.LinkedGap.linkedTo)
|
||||
{
|
||||
if (!(linked is Hull hull)) { continue; }
|
||||
if (hull.Volume > largestHullSize)
|
||||
{
|
||||
refHull = hull;
|
||||
largestHullSize = hull.Volume;
|
||||
}
|
||||
}
|
||||
if (refHull != null)
|
||||
{
|
||||
return IsHorizontal ?
|
||||
Math.Sign(Door.Item.WorldPosition.X - Door.LinkedGap.linkedTo[0].WorldPosition.X) :
|
||||
Math.Sign(Door.Item.WorldPosition.Y - Door.LinkedGap.linkedTo[0].WorldPosition.Y);
|
||||
Math.Sign(Door.Item.WorldPosition.X - refHull.WorldPosition.X) :
|
||||
Math.Sign(Door.Item.WorldPosition.Y - refHull.WorldPosition.Y);
|
||||
}
|
||||
else if (dockingTarget?.Door?.LinkedGap != null && dockingTarget.Door.LinkedGap.linkedTo.Count == 1)
|
||||
}
|
||||
if (dockingTarget?.Door?.LinkedGap != null && dockingTarget.Door.LinkedGap.linkedTo.Count > 0)
|
||||
{
|
||||
Hull refHull = null;
|
||||
float largestHullSize = 0.0f;
|
||||
foreach (MapEntity linked in dockingTarget.Door.LinkedGap.linkedTo)
|
||||
{
|
||||
if (!(linked is Hull hull)) { continue; }
|
||||
if (hull.Volume > largestHullSize)
|
||||
{
|
||||
refHull = hull;
|
||||
largestHullSize = hull.Volume;
|
||||
}
|
||||
}
|
||||
if (refHull != null)
|
||||
{
|
||||
return IsHorizontal ?
|
||||
Math.Sign(dockingTarget.Door.LinkedGap.linkedTo[0].WorldPosition.X - dockingTarget.Door.Item.WorldPosition.X) :
|
||||
Math.Sign(dockingTarget.Door.LinkedGap.linkedTo[0].WorldPosition.Y - dockingTarget.Door.Item.WorldPosition.Y);
|
||||
}
|
||||
Math.Sign(refHull.WorldPosition.X - dockingTarget.Door.Item.WorldPosition.X) :
|
||||
Math.Sign(refHull.WorldPosition.Y - dockingTarget.Door.Item.WorldPosition.Y);
|
||||
}
|
||||
}
|
||||
if (dockingTarget != null)
|
||||
{
|
||||
@@ -838,6 +866,7 @@ namespace Barotrauma.Items.Components
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
|
||||
{
|
||||
originalDockingTargetID = Entity.NullEntityID;
|
||||
item.CreateServerEvent(this);
|
||||
}
|
||||
#endif
|
||||
@@ -1010,9 +1039,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
|
||||
{
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null) return;
|
||||
#endif
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
bool wasDocked = docked;
|
||||
DockingPort prevDockingTarget = DockingTarget;
|
||||
@@ -1020,7 +1047,10 @@ namespace Barotrauma.Items.Components
|
||||
switch (connection.Name)
|
||||
{
|
||||
case "toggle":
|
||||
Docked = !docked;
|
||||
if (signal != "0")
|
||||
{
|
||||
Docked = !docked;
|
||||
}
|
||||
break;
|
||||
case "set_active":
|
||||
case "set_state":
|
||||
@@ -1044,16 +1074,5 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
|
||||
{
|
||||
msg.Write(docked);
|
||||
|
||||
if (docked)
|
||||
{
|
||||
msg.Write(DockingTarget.item.ID);
|
||||
msg.Write(hulls != null && hulls[0] != null && hulls[1] != null && gap != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,8 +90,10 @@ namespace Barotrauma.Items.Components
|
||||
get { return stuck; }
|
||||
set
|
||||
{
|
||||
if (isOpen || isBroken || !CanBeWelded) return;
|
||||
if (isOpen || isBroken || !CanBeWelded) { return; }
|
||||
stuck = MathHelper.Clamp(value, 0.0f, 100.0f);
|
||||
//don't allow clients to make the door stuck unless the server says so (handled in ClientRead)
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
if (stuck <= 0.0f) { IsStuck = false; }
|
||||
if (stuck >= 99.0f) { IsStuck = true; }
|
||||
}
|
||||
@@ -366,12 +368,24 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
Body.Enabled = Impassable || openState < 1.0f;
|
||||
bool wasEnabled = Body.Enabled;
|
||||
Body.Enabled = Impassable || openState < 1.0f;
|
||||
if (wasEnabled && !Body.Enabled && IsHorizontal)
|
||||
{
|
||||
//when opening a hatch, force characters above it to refresh the floor position
|
||||
//(otherwise the character won't fall through the hatch until it moves)
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.WorldPosition.Y < item.WorldPosition.Y) { continue; }
|
||||
if (c.WorldPosition.X < item.WorldRect.X || c.WorldPosition.X > item.WorldRect.Right) { continue; }
|
||||
c.AnimController?.ForceRefreshFloorY();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//don't use the predicted state here, because it might set
|
||||
//other items to an incorrect state if the prediction is wrong
|
||||
item.SendSignal(0, (isOpen) ? "1" : "0", "state_out", null);
|
||||
item.SendSignal(0, isOpen ? "1" : "0", "state_out", null);
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
@@ -616,12 +630,13 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
|
||||
{
|
||||
if (IsStuck) return;
|
||||
if (IsStuck) { return; }
|
||||
|
||||
bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value;
|
||||
|
||||
if (connection.Name == "toggle")
|
||||
{
|
||||
if (signal == "0") { return; }
|
||||
if (toggleCooldownTimer > 0.0f && sender != lastUser) { OnFailedToOpen(); return; }
|
||||
if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
|
||||
toggleCooldownTimer = ToggleCoolDown;
|
||||
|
||||
@@ -0,0 +1,832 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Vector2 = Microsoft.Xna.Framework.Vector2;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
internal class ProducedItem
|
||||
{
|
||||
[Serialize(0f, true)]
|
||||
public float Probability { get; set; }
|
||||
|
||||
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
|
||||
|
||||
public readonly ItemPrefab? Prefab;
|
||||
|
||||
public ProducedItem(ItemPrefab prefab, float probability)
|
||||
{
|
||||
Prefab = prefab;
|
||||
Probability = probability;
|
||||
}
|
||||
|
||||
public ProducedItem(XElement element)
|
||||
{
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
string itemIdentifier = element.GetAttributeString("identifier", string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(itemIdentifier))
|
||||
{
|
||||
Prefab = ItemPrefab.Find(null, itemIdentifier);
|
||||
}
|
||||
|
||||
LoadSubElements(element);
|
||||
}
|
||||
|
||||
private void LoadSubElements(XElement element)
|
||||
{
|
||||
if (!element.HasElements) { return; }
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "statuseffect":
|
||||
{
|
||||
StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name);
|
||||
if (effect.type != ActionType.OnProduceSpawned)
|
||||
{
|
||||
DebugConsole.ThrowError("Only OnProduceSpawned type can be used in <ProducedItem>.");
|
||||
continue;
|
||||
}
|
||||
|
||||
StatusEffects.Add(effect);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable UnusedMember.Global
|
||||
internal enum VineTileType
|
||||
{
|
||||
Stem = 0b0000,
|
||||
CrossJunction = 0b1111,
|
||||
VerticalLane = 0b1010,
|
||||
HorizontalLane = 0b0101,
|
||||
TurnTopRight = 0b1001,
|
||||
TurnTopLeft = 0b0011,
|
||||
TurnBottomLeft = 0b0110,
|
||||
TurnBottomRight = 0b1100,
|
||||
TSectionTop = 0b1011,
|
||||
TSectionLeft = 0b0111,
|
||||
TSectionBottom = 0b1110,
|
||||
TSectionRight = 0b1101,
|
||||
StumpTop = 0b0001,
|
||||
StumpLeft = 0b0010,
|
||||
StumpBottom = 0b0100,
|
||||
StumpRight = 0b1000
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum TileSide
|
||||
{
|
||||
None = 0,
|
||||
Top = 1 << 0,
|
||||
Left = 1 << 1,
|
||||
Bottom = 1 << 2,
|
||||
Right = 1 << 3
|
||||
}
|
||||
|
||||
internal struct FoliageConfig
|
||||
{
|
||||
public static FoliageConfig EmptyConfig = new FoliageConfig { Variant = -1, Rotation = 0f, Scale = 1.0f };
|
||||
public static readonly int EmptyConfigValue = EmptyConfig.Serialize();
|
||||
|
||||
public int Variant;
|
||||
public float Rotation;
|
||||
public float Scale;
|
||||
|
||||
public readonly int Serialize()
|
||||
{
|
||||
int variant = Math.Min(Variant + 1, 15);
|
||||
int scale = (int) (Scale * 10f);
|
||||
int rotation = (int) (Rotation / MathHelper.TwoPi * 10f);
|
||||
|
||||
return variant | (scale << 4) | (rotation << 8);
|
||||
}
|
||||
|
||||
public static FoliageConfig Deserialize(int value)
|
||||
{
|
||||
int variant = value & 0x00F;
|
||||
int scale = (value & 0x0F0) >> 4;
|
||||
int rotation = (value & 0xF00) >> 8;
|
||||
|
||||
return new FoliageConfig { Variant = variant - 1, Scale = scale / 10f, Rotation = rotation / 10f * MathHelper.TwoPi };
|
||||
}
|
||||
|
||||
public static FoliageConfig CreateRandomConfig(int maxVariants, float minScale, float maxScale, Random? random = null)
|
||||
{
|
||||
int flowerVariant = Growable.RandomInt(0, maxVariants, random);
|
||||
float flowerScale = (float) Growable.RandomDouble(minScale, maxScale, random);
|
||||
float flowerRotation = (float) Growable.RandomDouble(0, MathHelper.TwoPi, random);
|
||||
return new FoliageConfig { Variant = flowerVariant, Scale = flowerScale, Rotation = flowerRotation };
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class VineTile
|
||||
{
|
||||
public TileSide Sides = TileSide.None;
|
||||
public TileSide BlockedSides = TileSide.None;
|
||||
|
||||
public readonly FoliageConfig FlowerConfig;
|
||||
public readonly FoliageConfig LeafConfig;
|
||||
|
||||
public int FailedGrowthAttempts;
|
||||
public Rectangle Rect;
|
||||
public Vector2 Position;
|
||||
public Color HealthColor = Color.Transparent;
|
||||
public float DecayDelay;
|
||||
|
||||
private float VineStep;
|
||||
private float FlowerStep;
|
||||
private float growthStep;
|
||||
|
||||
public float GrowthStep
|
||||
{
|
||||
get => growthStep;
|
||||
set
|
||||
{
|
||||
const float limit = 1.0f;
|
||||
growthStep = value;
|
||||
VineStep = Math.Min((float) Math.Pow(value, 2), limit);
|
||||
if (value > limit)
|
||||
{
|
||||
FlowerStep = Math.Min((float) Math.Pow(value - limit, 2), limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly float diameter;
|
||||
private Vector2 offset;
|
||||
|
||||
private readonly Growable Parent;
|
||||
public VineTileType Type;
|
||||
|
||||
public readonly Dictionary<TileSide, Vector2> AdjacentPositions;
|
||||
|
||||
public static int Size = 32;
|
||||
|
||||
public VineTile(Growable parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null)
|
||||
{
|
||||
FlowerConfig = flowerConfig ?? FoliageConfig.EmptyConfig;
|
||||
LeafConfig = leafConfig ?? FoliageConfig.EmptyConfig;
|
||||
Position = position;
|
||||
Rect = rect ?? CreatePlantRect(position);
|
||||
Parent = parent;
|
||||
Type = type;
|
||||
diameter = Rect.Width / 2.0f;
|
||||
|
||||
AdjacentPositions = new Dictionary<TileSide, Vector2>
|
||||
{
|
||||
{ TileSide.Top, new Vector2(Position.X, Position.Y + Rect.Height) },
|
||||
{ TileSide.Bottom, new Vector2(Position.X, Position.Y - Rect.Height) },
|
||||
{ TileSide.Left, new Vector2(Position.X - Rect.Width, Position.Y) },
|
||||
{ TileSide.Right, new Vector2(Position.X + Rect.Width, Position.Y) }
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateScale(float deltaTime)
|
||||
{
|
||||
if (Parent.Decayed && GrowthStep > 1.0f)
|
||||
{
|
||||
if (DecayDelay > 0)
|
||||
{
|
||||
DecayDelay -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
GrowthStep -= 0.25f * deltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (GrowthStep >= 2.0f || Parent.Decayed) { return; }
|
||||
|
||||
GrowthStep += deltaTime;
|
||||
|
||||
if (GrowthStep < 1.0f)
|
||||
{
|
||||
// I don't know how or why this works
|
||||
float offsetAmount = diameter * VineStep - diameter;
|
||||
switch (Type)
|
||||
{
|
||||
case VineTileType.StumpLeft:
|
||||
offset.X = offsetAmount;
|
||||
break;
|
||||
case VineTileType.StumpRight:
|
||||
offset.X = -offsetAmount;
|
||||
break;
|
||||
case VineTileType.StumpTop:
|
||||
offset.Y = offsetAmount;
|
||||
break;
|
||||
case VineTileType.Stem:
|
||||
case VineTileType.StumpBottom:
|
||||
offset.Y = -offsetAmount;
|
||||
break;
|
||||
default:
|
||||
offset = Vector2.Zero;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
offset = Vector2.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 GetWorldPosition(Planter planter, Vector2 slotOffset)
|
||||
{
|
||||
return planter.Item.WorldPosition + slotOffset + Position;
|
||||
}
|
||||
|
||||
public void UpdateType()
|
||||
{
|
||||
if (Type == VineTileType.Stem) { return; }
|
||||
|
||||
Type = (VineTileType) Sides;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a random side that is not occupied.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There is probably a much better way of doing this than allocating memory with an array
|
||||
/// but this felt like the most reliable approach I could come up with.
|
||||
/// </remarks>
|
||||
/// <returns></returns>
|
||||
public TileSide GetRandomFreeSide(Random? random = null)
|
||||
{
|
||||
const int maxSides = 4;
|
||||
TileSide occupiedSides = Sides | BlockedSides;
|
||||
int setBits = occupiedSides.Count();
|
||||
if (setBits >= maxSides) { return TileSide.None; }
|
||||
|
||||
int possible = maxSides - setBits;
|
||||
int[] pool = new int[possible];
|
||||
|
||||
for (int i = 0, j = 0; i < maxSides; i++)
|
||||
{
|
||||
if (!occupiedSides.IsBitSet((TileSide) (1 << i)))
|
||||
{
|
||||
pool[j] = i;
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
int value = pool[Growable.RandomInt(0, possible, random)];
|
||||
|
||||
return (TileSide) (1 << value);
|
||||
}
|
||||
|
||||
public bool CanGrowMore() => (Sides | BlockedSides).Count() < 4;
|
||||
|
||||
public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int) pos.X - Size / 2, (int) pos.Y + Size / 2, Size, Size);
|
||||
}
|
||||
|
||||
internal static class GrowthSideExtension
|
||||
{
|
||||
// Enum.HasFlag() sucks
|
||||
public static bool IsBitSet(this TileSide side, TileSide bit)
|
||||
{
|
||||
return ((int) side & (int) bit) != 0;
|
||||
}
|
||||
|
||||
// K&R algorithm for counting how many bits are set in a bit field
|
||||
public static int Count(this TileSide side)
|
||||
{
|
||||
int n = (int) side;
|
||||
int count = 0;
|
||||
while (n != 0)
|
||||
{
|
||||
count += n & 1;
|
||||
n >>= 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class Growable : ItemComponent, IServerSerializable
|
||||
{
|
||||
// used for debugging where a vine failed to grow
|
||||
public readonly HashSet<Rectangle> FailedRectangles = new HashSet<Rectangle>();
|
||||
|
||||
[Serialize(1f, true, "How fast the plant grows.")]
|
||||
public float GrowthSpeed { get; set; }
|
||||
|
||||
[Serialize(100f, true, "How long the plant can go without watering.")]
|
||||
public float MaxHealth { get; set; }
|
||||
|
||||
[Serialize(1f, true, "How much damage the plant takes while in water.")]
|
||||
public float FloodTolerance { get; set; }
|
||||
|
||||
[Serialize(1f, true, "How much damage the plant takes while growing.")]
|
||||
public float Hardiness { get; set; }
|
||||
|
||||
[Serialize(0.01f, true, "How often a seed is produced.")]
|
||||
public float SeedRate { get; set; }
|
||||
|
||||
[Serialize(0.01f, true, "How often a product item is produced.")]
|
||||
public float ProductRate { get; set; }
|
||||
|
||||
[Serialize(0.5f, true, "Probability of an attribute being randomly modified in a newly produced seed.")]
|
||||
public float MutationProbability { get; set; }
|
||||
|
||||
[Serialize("1.0,1.0,1.0,1.0", true, "Color of the flowers.")]
|
||||
public Color FlowerTint { get; set; }
|
||||
|
||||
[Serialize(3, true, "Number of flowers drawn when fully grown")]
|
||||
public int FlowerQuantity { get; set; }
|
||||
|
||||
[Serialize(0.25f, true, "Size of the flower sprites.")]
|
||||
public float BaseFlowerScale { get; set; }
|
||||
|
||||
[Serialize(0.5f, true, "Size of the leaf sprites.")]
|
||||
public float BaseLeafScale { get; set; }
|
||||
|
||||
[Serialize("1.0,1.0,1.0,1.0", true, "Color of the leaves.")]
|
||||
public Color LeafTint { get; set; }
|
||||
|
||||
[Serialize(0.33f, true, "Chance of a leaf appearing behind a branch.")]
|
||||
public float LeafProbability { get; set; }
|
||||
|
||||
[Serialize("1.0,1.0,1.0,1.0", true, "Color of the vines.")]
|
||||
public Color VineTint { get; set; }
|
||||
|
||||
[Serialize(32, true, "Maximum number of vine tiles the plant can grow.")]
|
||||
public int MaximumVines { get; set; }
|
||||
|
||||
[Serialize(0.25f, true, "Size of the vine sprites.")]
|
||||
public float VineScale { get; set; }
|
||||
|
||||
[Serialize("0.26,0.27,0.29,1.0", true, "Tint of a dead plant.")]
|
||||
public Color DeadTint { get; set; }
|
||||
|
||||
private const float increasedDeathSpeed = 10f;
|
||||
private bool accelerateDeath;
|
||||
private float health;
|
||||
private int flowerVariants;
|
||||
private int leafVariants;
|
||||
private int[] flowerTiles;
|
||||
|
||||
public float Health
|
||||
{
|
||||
get => health;
|
||||
set => health = Math.Clamp(value, 0, MaxHealth);
|
||||
}
|
||||
|
||||
public bool Decayed;
|
||||
public bool FullyGrown;
|
||||
|
||||
private const int maxProductDelay = 10,
|
||||
maxVineGrowthDelay = 10;
|
||||
|
||||
private int productDelay;
|
||||
private int vineDelay;
|
||||
|
||||
public readonly List<ProducedItem> ProducedItems = new List<ProducedItem>();
|
||||
public readonly List<VineTile> Vines = new List<VineTile>();
|
||||
private readonly ProducedItem ProducedSeed;
|
||||
|
||||
private static float MinFlowerScale = 0.5f, MaxFlowerScale = 1.0f, MinLeafScale = 0.5f, MaxLeafScale = 1.0f;
|
||||
private const int VineChunkSize = 32;
|
||||
|
||||
public Growable(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
Health = MaxHealth;
|
||||
|
||||
if (element.HasElements)
|
||||
{
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "produceditem":
|
||||
ProducedItems.Add(new ProducedItem(subElement));
|
||||
break;
|
||||
case "vinesprites":
|
||||
LoadVines(subElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProducedSeed = new ProducedItem(this.item.Prefab, 1.0f);
|
||||
flowerTiles = new int[FlowerQuantity];
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
if (flowerTiles.All(i => i == 0))
|
||||
{
|
||||
GenerateFlowerTiles();
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateFlowerTiles(Random? random = null)
|
||||
{
|
||||
flowerTiles = new int[FlowerQuantity];
|
||||
List<int> pool = new List<int>();
|
||||
for (int i = 0; i < MaximumVines - 1; i++) { pool.Add(i); }
|
||||
|
||||
for (int i = 0; i < flowerTiles.Length; i++)
|
||||
{
|
||||
int index = RandomInt(0, pool.Count, random);
|
||||
flowerTiles[i] = pool[index];
|
||||
pool.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
partial void LoadVines(XElement element);
|
||||
|
||||
public void OnGrowthTick(Planter planter, PlantSlot slot)
|
||||
{
|
||||
if (Decayed) { return; }
|
||||
|
||||
if (FullyGrown)
|
||||
{
|
||||
TryGenerateProduct(planter, slot);
|
||||
}
|
||||
|
||||
if (Health > 0)
|
||||
{
|
||||
GrowVines(planter, slot);
|
||||
Health -= accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness;
|
||||
|
||||
if (planter.Item.InWater)
|
||||
{
|
||||
Health -= FloodTolerance;
|
||||
}
|
||||
}
|
||||
|
||||
CheckPlantState();
|
||||
|
||||
#if CLIENT
|
||||
UpdateBranchHealth();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void UpdateBranchHealth()
|
||||
{
|
||||
Color healthColor = Color.White * (1.0f - Health / MaxHealth);
|
||||
foreach (VineTile vine in Vines)
|
||||
{
|
||||
vine.HealthColor = healthColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryGenerateProduct(Planter planter, PlantSlot slot)
|
||||
{
|
||||
productDelay++;
|
||||
if (productDelay <= maxProductDelay) { return; }
|
||||
|
||||
productDelay = 0;
|
||||
|
||||
bool spawnProduct = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < ProductRate,
|
||||
spawnSeed = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < SeedRate;
|
||||
|
||||
Vector2 spawnPos;
|
||||
|
||||
if (spawnProduct || spawnSeed)
|
||||
{
|
||||
VineTile vine = Vines.GetRandom();
|
||||
spawnPos = vine.GetWorldPosition(planter, slot.Offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (spawnProduct && ProducedItems.Any())
|
||||
{
|
||||
SpawnItem(ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos);
|
||||
return;
|
||||
}
|
||||
|
||||
if (spawnSeed)
|
||||
{
|
||||
SpawnItem(ProducedSeed, spawnPos);
|
||||
}
|
||||
|
||||
static void SpawnItem(ProducedItem producedItem, Vector2 pos)
|
||||
{
|
||||
if (producedItem.Prefab == null) { return; }
|
||||
|
||||
Entity.Spawner?.AddToSpawnQueue(producedItem.Prefab, pos, onSpawned: it =>
|
||||
{
|
||||
foreach (StatusEffect effect in producedItem.StatusEffects)
|
||||
{
|
||||
it.ApplyStatusEffect(effect, ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true);
|
||||
}
|
||||
|
||||
it.ApplyStatusEffects(ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates plant's state to fully grown or dead depending on its conditions.
|
||||
/// </summary>
|
||||
/// <returns>True if the plant has finished growing.</returns>
|
||||
private bool CheckPlantState()
|
||||
{
|
||||
if (Decayed) { return true; }
|
||||
|
||||
if (0 >= Health)
|
||||
{
|
||||
Decayed = true;
|
||||
#if CLIENT
|
||||
foreach (VineTile vine in Vines)
|
||||
{
|
||||
vine.DecayDelay = (float) RandomDouble(0f, 30f);
|
||||
}
|
||||
#endif
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Vines.Count >= MaximumVines && !FullyGrown)
|
||||
{
|
||||
FullyGrown = true;
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!FullyGrown && !accelerateDeath && Vines.Any() && Vines.All(tile => !tile.CanGrowMore()))
|
||||
{
|
||||
accelerateDeath = true;
|
||||
}
|
||||
|
||||
// if the player somehow finds a way to extract the seed out of a planter kill the plant
|
||||
if (item.ParentInventory is CharacterInventory)
|
||||
{
|
||||
Decayed = true;
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
base.Update(deltaTime, cam);
|
||||
|
||||
#if CLIENT
|
||||
foreach (VineTile vine in Vines)
|
||||
{
|
||||
vine.UpdateScale(deltaTime);
|
||||
}
|
||||
#endif
|
||||
|
||||
CheckPlantState();
|
||||
}
|
||||
|
||||
private void GrowVines(Planter planter, PlantSlot slot)
|
||||
{
|
||||
if (FullyGrown) { return; }
|
||||
|
||||
vineDelay++;
|
||||
if (vineDelay <= maxVineGrowthDelay / GrowthSpeed) { return; }
|
||||
|
||||
vineDelay = 0;
|
||||
|
||||
if (!Vines.Any())
|
||||
{
|
||||
// generate first stem
|
||||
GenerateStem();
|
||||
return;
|
||||
}
|
||||
|
||||
int count = Vines.Count;
|
||||
|
||||
TryGenerateBranches(planter, slot);
|
||||
|
||||
if (Vines.Count > count)
|
||||
{
|
||||
#if SERVER
|
||||
for (int i = 0; i < Vines.Count; i += VineChunkSize)
|
||||
{
|
||||
GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), i });
|
||||
}
|
||||
#elif CLIENT
|
||||
ResetPlanterSize();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateStem()
|
||||
{
|
||||
VineTile stem = new VineTile(this, Vector2.Zero, VineTileType.Stem) { BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right };
|
||||
Vines.Add(stem);
|
||||
}
|
||||
|
||||
private void TryGenerateBranches(Planter planter, PlantSlot slot, Random? random = null, Random? flowerRandom = null)
|
||||
{
|
||||
List<VineTile> newList = new List<VineTile>(Vines);
|
||||
foreach (VineTile oldVines in newList)
|
||||
{
|
||||
if (oldVines.FailedGrowthAttempts > 8 || !oldVines.CanGrowMore()) { continue; }
|
||||
|
||||
if (RandomInt(0, Vines.Count(tile => tile.CanGrowMore()), random) != 0) { continue; }
|
||||
|
||||
TileSide side = oldVines.GetRandomFreeSide(random);
|
||||
|
||||
if (side == TileSide.None) { continue; }
|
||||
|
||||
Vector2 pos = oldVines.AdjacentPositions[side];
|
||||
Rectangle rect = VineTile.CreatePlantRect(pos);
|
||||
|
||||
if (CollidesWithWorld(rect, planter, slot))
|
||||
{
|
||||
oldVines.BlockedSides |= side;
|
||||
oldVines.FailedGrowthAttempts++;
|
||||
continue;
|
||||
}
|
||||
|
||||
FoliageConfig flowerConfig = FoliageConfig.EmptyConfig;
|
||||
FoliageConfig leafConfig = FoliageConfig.EmptyConfig;
|
||||
|
||||
if (flowerTiles.Any(i => Vines.Count == i))
|
||||
{
|
||||
flowerConfig = FoliageConfig.CreateRandomConfig(flowerVariants, MinFlowerScale, MaxFlowerScale, flowerRandom);
|
||||
}
|
||||
|
||||
if (LeafProbability >= RandomDouble(0d, 1.0d, flowerRandom) && leafVariants > 0)
|
||||
{
|
||||
leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, MinLeafScale, MaxLeafScale, flowerRandom);
|
||||
}
|
||||
|
||||
VineTile newVine = new VineTile(this, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect);
|
||||
|
||||
foreach (VineTile otherVine in Vines)
|
||||
{
|
||||
var (distX, distY) = pos - otherVine.Position;
|
||||
int absDistX = (int) Math.Abs(distX), absDistY = (int) Math.Abs(distY);
|
||||
|
||||
// check if the tile is within the with or height distance from us but ignore diagonals
|
||||
if (absDistX > newVine.Rect.Width || absDistY > newVine.Rect.Height || absDistX > 0 && absDistY > 0) { continue; }
|
||||
|
||||
// determines what side the tile is relative to the new tile by comparing the X/Y distance values
|
||||
// if the X value is bigger than Y it's to the left or right of us and then check if X is negative or positive to determine if it's right or left
|
||||
TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom;
|
||||
|
||||
// We use log2 to find the index and offset that index by 2 since the opposite side is always 2 offsets away
|
||||
TileSide oppositeSide = (TileSide) (1 << ((int) Math.Log2((int) connectingSide) + 2) % 4);
|
||||
|
||||
if (otherVine.BlockedSides.IsBitSet(connectingSide))
|
||||
{
|
||||
newVine.BlockedSides |= oppositeSide;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (otherVine != oldVines)
|
||||
{
|
||||
otherVine.BlockedSides |= connectingSide;
|
||||
newVine.BlockedSides |= oppositeSide;
|
||||
}
|
||||
else
|
||||
{
|
||||
otherVine.Sides |= connectingSide;
|
||||
newVine.Sides |= oppositeSide;
|
||||
}
|
||||
}
|
||||
|
||||
Vines.Add(newVine);
|
||||
|
||||
foreach (VineTile vine in Vines)
|
||||
{
|
||||
vine.UpdateType();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CollidesWithWorld(Rectangle rect, Planter planter, PlantSlot slot)
|
||||
{
|
||||
if (Vines.Any(g => g.Rect.Contains(rect))) { return true; }
|
||||
|
||||
Rectangle worldRect = rect;
|
||||
worldRect.Location = planter.Item.WorldPosition.ToPoint() + slot.Offset.ToPoint() + worldRect.Location;
|
||||
worldRect.Y -= worldRect.Height;
|
||||
|
||||
Rectangle planterRect = planter.Item.WorldRect;
|
||||
planterRect.Y -= planterRect.Height;
|
||||
|
||||
if (planterRect.Intersects(worldRect))
|
||||
{
|
||||
#if DEBUG
|
||||
if (!FailedRectangles.Contains(worldRect))
|
||||
{
|
||||
FailedRectangles.Add(worldRect);
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector2 topLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Top)),
|
||||
topRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Top)),
|
||||
bottomLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Bottom)),
|
||||
bottomRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Bottom));
|
||||
|
||||
// ray casting a cross on the corners didn't seem to work so we are ray casting along the perimeter
|
||||
bool hasCollision = planterRect.Intersects(worldRect) || LineCollides(topLeft, topRight) || LineCollides(topRight, bottomRight) || LineCollides(bottomRight, bottomLeft) || LineCollides(bottomLeft, topLeft);
|
||||
|
||||
#if DEBUG
|
||||
if (hasCollision)
|
||||
{
|
||||
if (!FailedRectangles.Contains(worldRect))
|
||||
{
|
||||
FailedRectangles.Add(worldRect);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return hasCollision;
|
||||
|
||||
static bool LineCollides(Vector2 point1, Vector2 point2)
|
||||
{
|
||||
const Category category = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
|
||||
return Submarine.PickBody(point1, point2, collisionCategory: category, customPredicate: f => !(f.UserData is Hull) && f.CollidesWith.HasFlag(Physics.CollisionItem)) != null;
|
||||
}
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
XElement element = base.Save(parentElement);
|
||||
element.Add(new XAttribute("flowertiles", string.Join(",", flowerTiles)));
|
||||
element.Add(new XAttribute("decayed", Decayed));
|
||||
foreach (VineTile vine in Vines)
|
||||
{
|
||||
XElement vineElement = new XElement("Vine");
|
||||
vineElement.Add(new XAttribute("sides", (int) vine.Sides));
|
||||
vineElement.Add(new XAttribute("blockedsides", (int) vine.BlockedSides));
|
||||
vineElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(vine.Position)));
|
||||
vineElement.Add(new XAttribute("tile", (int) vine.Type));
|
||||
vineElement.Add(new XAttribute("failedattempts", vine.FailedGrowthAttempts));
|
||||
#if SERVER
|
||||
vineElement.Add(new XAttribute("growthscale", Decayed ? 1.0f : 2.0f));
|
||||
#else
|
||||
vineElement.Add(new XAttribute("growthscale", vine.GrowthStep));
|
||||
#endif
|
||||
vineElement.Add(new XAttribute("flowerconfig", vine.FlowerConfig.Serialize()));
|
||||
vineElement.Add(new XAttribute("leafconfig", vine.LeafConfig.Serialize()));
|
||||
|
||||
element.Add(vineElement);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
public override void Load(XElement componentElement, bool usePrefabValues)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues);
|
||||
flowerTiles = componentElement.GetAttributeIntArray("flowertiles", new int[0]);
|
||||
Decayed = componentElement.GetAttributeBool("decayed", false);
|
||||
|
||||
Vines.Clear();
|
||||
foreach (XElement element in componentElement.Elements())
|
||||
{
|
||||
if (element.Name.ToString().Equals("vine", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
VineTileType type = (VineTileType) element.GetAttributeInt("tile", 0);
|
||||
Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero);
|
||||
TileSide sides = (TileSide) element.GetAttributeInt("sides", 0);
|
||||
TileSide blockedSides = (TileSide) element.GetAttributeInt("blockedsides", 0);
|
||||
int failedAttempts = element.GetAttributeInt("failedattempts", 0);
|
||||
float growthscale = element.GetAttributeFloat("growthscale", 0f);
|
||||
int flowerConfig = element.GetAttributeInt("flowerconfig", FoliageConfig.EmptyConfigValue);
|
||||
int leafConfig = element.GetAttributeInt("leafconfig", FoliageConfig.EmptyConfigValue);
|
||||
|
||||
VineTile tile = new VineTile(this, pos, type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig))
|
||||
{
|
||||
Sides = sides, BlockedSides = blockedSides, FailedGrowthAttempts = failedAttempts, GrowthStep = growthscale
|
||||
};
|
||||
|
||||
Vines.Add(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanGrowMore() => Vines.Any(tile => tile.CanGrowMore());
|
||||
|
||||
public static int RandomInt(int min, int max, Random? random = null) => random?.Next(min, max) ?? Rand.Range(min, max);
|
||||
public static double RandomDouble(double min, double max, Random? random = null) => random?.NextDouble() * (max - min) + min ?? Rand.Range(min, max);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics.Contacts;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
@@ -392,6 +393,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (item.GetComponent<LevelResource>() != null) { return true; }
|
||||
|
||||
if (item.GetComponent<Planter>() is { } planter && planter.GrowableSeeds.Any(seed => seed != null)) { return false; }
|
||||
|
||||
//if the item has a connection panel and rewiring is disabled, don't allow deattaching
|
||||
var connectionPanel = item.GetComponent<ConnectionPanel>();
|
||||
if (connectionPanel != null && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true)))
|
||||
@@ -476,12 +479,13 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
var containedItems = item.ContainedItems;
|
||||
var containedItems = item.OwnInventory?.Items;
|
||||
if (containedItems != null)
|
||||
{
|
||||
foreach (Item contained in containedItems)
|
||||
{
|
||||
if (contained.body == null) continue;
|
||||
if (contained == null) { continue; }
|
||||
if (contained.body == null) { continue; }
|
||||
contained.SetTransform(item.SimPosition, contained.body.Rotation);
|
||||
}
|
||||
}
|
||||
@@ -573,7 +577,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (item.body == null || !item.body.Enabled) return;
|
||||
if (item.body == null || !item.body.Enabled) { return; }
|
||||
if (picker == null || !picker.HasEquippedItem(item))
|
||||
{
|
||||
if (Pusher != null) { Pusher.Enabled = false; }
|
||||
@@ -598,7 +602,10 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
|
||||
|
||||
if (item.body.Dir != picker.AnimController.Dir) Flip();
|
||||
if (item.body.Dir != picker.AnimController.Dir)
|
||||
{
|
||||
item.FlipX(relativeToSub: false);
|
||||
}
|
||||
|
||||
item.Submarine = picker.Submarine;
|
||||
|
||||
@@ -635,11 +642,14 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public void Flip()
|
||||
public override void FlipX(bool relativeToSub)
|
||||
{
|
||||
handlePos[0].X = -handlePos[0].X;
|
||||
handlePos[1].X = -handlePos[1].X;
|
||||
item.body.Dir = -item.body.Dir;
|
||||
if (item.body != null)
|
||||
{
|
||||
item.body.Dir = -item.body.Dir;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
|
||||
@@ -63,6 +63,13 @@ namespace Barotrauma.Items.Components
|
||||
item.RequireAimToUse = true;
|
||||
}
|
||||
|
||||
public override void Equip(Character character)
|
||||
{
|
||||
base.Equip(character);
|
||||
reloadTimer = Math.Min(reload, 1.0f);
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
if (character == null || reloadTimer > 0.0f) { return false; }
|
||||
@@ -151,7 +158,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
|
||||
|
||||
if (item.body.Dir != picker.AnimController.Dir) { Flip(); }
|
||||
if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); }
|
||||
|
||||
AnimController ac = picker.AnimController;
|
||||
|
||||
@@ -366,13 +373,15 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
bool success = Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(User);
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits
|
||||
{
|
||||
GameMain.Server.CreateEntityEvent(item, new object[]
|
||||
{
|
||||
Networking.NetEntityEvent.Type.ApplyStatusEffect,
|
||||
ActionType.OnUse,
|
||||
success ? ActionType.OnUse : ActionType.OnFailure,
|
||||
null, //itemcomponent
|
||||
targetCharacter.ID, targetLimb
|
||||
});
|
||||
@@ -389,7 +398,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: User);
|
||||
ApplyStatusEffects(success ? ActionType.OnUse : ActionType.OnFailure, 1.0f, targetCharacter, targetLimb, user: User);
|
||||
}
|
||||
|
||||
if (DeleteOnUse)
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace Barotrauma.Items.Components
|
||||
allowedSlots.Add(allowedSlot);
|
||||
}
|
||||
|
||||
canBePicked = true;
|
||||
canBePicked = true;
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
@@ -142,7 +142,8 @@ namespace Barotrauma.Items.Components
|
||||
this,
|
||||
item.WorldPosition,
|
||||
pickTimer / requiredTime,
|
||||
GUI.Style.Red, GUI.Style.Green);
|
||||
GUI.Style.Red, GUI.Style.Green,
|
||||
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
|
||||
#endif
|
||||
|
||||
picker.AnimController.UpdateUseItem(true, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
|
||||
|
||||
@@ -72,6 +72,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public override void Equip(Character character)
|
||||
{
|
||||
reloadTimer = Math.Min(reload, 1.0f);
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
reloadTimer -= deltaTime;
|
||||
@@ -180,22 +186,25 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public Projectile FindProjectile(bool triggerOnUseOnContainers = false)
|
||||
{
|
||||
var containedItems = item.ContainedItems;
|
||||
var containedItems = item.OwnInventory?.Items;
|
||||
if (containedItems == null) { return null; }
|
||||
|
||||
foreach (Item item in containedItems)
|
||||
{
|
||||
if (item == null) { continue; }
|
||||
Projectile projectile = item.GetComponent<Projectile>();
|
||||
if (projectile != null) { return projectile; }
|
||||
}
|
||||
|
||||
//projectile not found, see if one of the contained items contains projectiles
|
||||
foreach (Item item in containedItems)
|
||||
foreach (Item it in containedItems)
|
||||
{
|
||||
var containedSubItems = item.ContainedItems;
|
||||
if (it == null) { continue; }
|
||||
var containedSubItems = it.OwnInventory?.Items;
|
||||
if (containedSubItems == null) { continue; }
|
||||
foreach (Item subItem in containedSubItems)
|
||||
{
|
||||
if (subItem == null) { continue; }
|
||||
Projectile projectile = subItem.GetComponent<Projectile>();
|
||||
//apply OnUse statuseffects to the container in case it has to react to it somehow
|
||||
//(play a sound, spawn more projectiles, reduce condition...)
|
||||
|
||||
@@ -52,12 +52,16 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false, description: "How much the item decreases the size of fires per second.")]
|
||||
public float ExtinguishAmount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false, description: "How much water the item provides to planters per second.")]
|
||||
public float WaterAmount { get; set; }
|
||||
|
||||
[Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels).")]
|
||||
public Vector2 BarrelPos { get; set; }
|
||||
|
||||
@@ -82,13 +86,19 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(0.0f, false, description: "Force applied to the entity the ray hits.")]
|
||||
public float TargetForce { get; set; }
|
||||
|
||||
[Serialize(0.0f, false, description: "Rotation of the barrel in degrees."), Editable(MinValueFloat = 0, MaxValueFloat = 360, VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" })]
|
||||
public float BarrelRotation
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Vector2 TransformedBarrelPos
|
||||
{
|
||||
get
|
||||
{
|
||||
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation);
|
||||
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation + MathHelper.ToRadians(BarrelRotation));
|
||||
Vector2 flippedPos = BarrelPos;
|
||||
if (item.body.Dir < 0.0f) flippedPos.X = -flippedPos.X;
|
||||
if (item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; }
|
||||
return (Vector2.Transform(flippedPos, bodyTransform));
|
||||
}
|
||||
}
|
||||
@@ -188,7 +198,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
float spread = MathHelper.ToRadians(MathHelper.Lerp(UnskilledSpread, Spread, degreeOfSuccess));
|
||||
float angle = item.body.Rotation + spread * Rand.Range(-0.5f, 0.5f);
|
||||
float angle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + spread * Rand.Range(-0.5f, 0.5f);
|
||||
Vector2 rayEnd = rayStart +
|
||||
ConvertUnits.ToSimUnits(new Vector2(
|
||||
(float)Math.Cos(angle),
|
||||
@@ -276,7 +286,7 @@ namespace Barotrauma.Items.Components
|
||||
ignoreSensors: false,
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; }
|
||||
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure || (f.Body?.UserData is Item it && it.GetComponent<Planter>() != null)) { return false; }
|
||||
if (f.Body?.UserData as string == "ruinroom") { return false; }
|
||||
return true;
|
||||
},
|
||||
@@ -373,6 +383,42 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
if (WaterAmount > 0.0f && item.CurrentHull?.Submarine != null)
|
||||
{
|
||||
Vector2 pos = ConvertUnits.ToDisplayUnits(rayStart + item.Submarine.SimPosition);
|
||||
|
||||
// Could probably be done much efficiently here
|
||||
foreach (Item it in Item.ItemList)
|
||||
{
|
||||
if (it.Submarine == item.Submarine && it.GetComponent<Planter>() is { } planter)
|
||||
{
|
||||
if (it.GetComponent<Holdable>() is { } holdable && holdable.Attachable && !holdable.Attached) { continue; }
|
||||
|
||||
Rectangle collisionRect = it.WorldRect;
|
||||
collisionRect.Y -= collisionRect.Height;
|
||||
if (collisionRect.Left < pos.X && collisionRect.Right > pos.X && collisionRect.Bottom < pos.Y)
|
||||
{
|
||||
Body collision = Submarine.PickBody(rayStart, it.SimPosition, ignoredBodies, collisionCategories);
|
||||
if (collision == null)
|
||||
{
|
||||
for (var i = 0; i < planter.GrowableSeeds.Length; i++)
|
||||
{
|
||||
Growable seed = planter.GrowableSeeds[i];
|
||||
if (seed == null || seed.Decayed) { continue; }
|
||||
|
||||
seed.Health += WaterAmount * deltaTime;
|
||||
|
||||
#if CLIENT
|
||||
float barOffset = 10f * GUI.Scale;
|
||||
Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero;
|
||||
user.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUI.Style.Blue, GUI.Style.Blue, "progressbar.watering");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
@@ -464,7 +510,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (targetBody.UserData is Item targetItem)
|
||||
{
|
||||
if (!HitItems) { return false; }
|
||||
if (!HitItems || targetItem.NonInteractable) { return false; }
|
||||
|
||||
var levelResource = targetItem.GetComponent<LevelResource>();
|
||||
if (levelResource != null && levelResource.Attached &&
|
||||
@@ -477,8 +523,9 @@ namespace Barotrauma.Items.Components
|
||||
this,
|
||||
targetItem.WorldPosition,
|
||||
levelResource.DeattachTimer / levelResource.DeattachDuration,
|
||||
GUI.Style.Red, GUI.Style.Green);
|
||||
GUI.Style.Red, GUI.Style.Green, "progressbar.deattaching");
|
||||
#endif
|
||||
FixItemProjSpecific(user, deltaTime, targetItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -571,34 +618,31 @@ namespace Barotrauma.Items.Components
|
||||
character.AIController.SteeringManager.SteeringSeek(standPos);
|
||||
}
|
||||
}
|
||||
else
|
||||
if (dist < reach / 2)
|
||||
{
|
||||
if (dist < reach / 2)
|
||||
// Too close -> steer away
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
|
||||
}
|
||||
else if (dist < reach * 2)
|
||||
{
|
||||
// In or almost in range
|
||||
character.CursorPosition = leak.Position;
|
||||
character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
// Too close -> steer away
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
|
||||
}
|
||||
else if (dist <= reach)
|
||||
{
|
||||
// In range
|
||||
character.CursorPosition = leak.Position;
|
||||
character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
var torso = character.AnimController.GetLimb(LimbType.Torso);
|
||||
// Turn facing the target when not moving (handled in the animcontroller if not moving)
|
||||
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
|
||||
Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir;
|
||||
float newRotation = MathUtils.VectorToAngle(diff);
|
||||
character.AnimController.Collider.SmoothRotate(newRotation, 5.0f);
|
||||
var torso = character.AnimController.GetLimb(LimbType.Torso);
|
||||
// Turn facing the target when not moving (handled in the animcontroller if not moving)
|
||||
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
|
||||
Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir;
|
||||
float newRotation = MathUtils.VectorToAngle(diff);
|
||||
character.AnimController.Collider.SmoothRotate(newRotation, 5.0f);
|
||||
|
||||
if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4)
|
||||
{
|
||||
// Swim past
|
||||
Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX;
|
||||
moveDir *= character.AnimController.Dir;
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
|
||||
}
|
||||
if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4)
|
||||
{
|
||||
// Swim past
|
||||
Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX;
|
||||
moveDir *= character.AnimController.Dir;
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -674,9 +718,8 @@ namespace Barotrauma.Items.Components
|
||||
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
|
||||
foreach (ISerializableEntity target in targets)
|
||||
{
|
||||
if (!(target is Door door)) { continue; }
|
||||
|
||||
if (!door.CanBeWelded) { continue; }
|
||||
if (!(target is Door door)) { continue; }
|
||||
if (!door.CanBeWelded || door.Item.NonInteractable) { continue; }
|
||||
for (int i = 0; i < effect.propertyNames.Length; i++)
|
||||
{
|
||||
string propertyName = effect.propertyNames[i];
|
||||
@@ -685,7 +728,7 @@ namespace Barotrauma.Items.Components
|
||||
object value = property.GetValue(target);
|
||||
if (door.Stuck > 0)
|
||||
{
|
||||
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White);
|
||||
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White, "progressbar.welding");
|
||||
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Sprayer : RangedWeapon
|
||||
{
|
||||
[Serialize(0.0f, false, description: "The distance at which the item can spray walls.")]
|
||||
public float Range { get; set; }
|
||||
|
||||
[Serialize(1.0f, false, description: "How fast the item changes the color of the walls.")]
|
||||
public float SprayStrength { get; set; }
|
||||
|
||||
private readonly Dictionary<string, Color> liquidColors;
|
||||
private ItemContainer liquidContainer;
|
||||
|
||||
public Sprayer(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
item.IsShootable = true;
|
||||
item.RequireAimToUse = true;
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "paintcolors":
|
||||
{
|
||||
liquidColors = new Dictionary<string, Color>();
|
||||
foreach (XElement paintElement in subElement.Elements())
|
||||
{
|
||||
string paintName = paintElement.GetAttributeString("paintitem", string.Empty);
|
||||
Color paintColor = paintElement.GetAttributeColor("color", Color.Transparent);
|
||||
|
||||
if (paintName != string.Empty)
|
||||
{
|
||||
liquidColors.Add(paintName, paintColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
liquidContainer = item.GetComponent<ItemContainer>();
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
#if SERVER
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
return character != null || character.Removed;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
|
||||
|
||||
if (item.body.Dir != picker.AnimController.Dir) { Flip(); }
|
||||
if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); }
|
||||
|
||||
AnimController ac = picker.AnimController;
|
||||
|
||||
|
||||
@@ -76,6 +76,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("", false, description: "What to display on the progress bar when this item is being picked.")]
|
||||
public string PickingMsg
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties { get; protected set; }
|
||||
|
||||
public Action<bool> OnActiveStateChanged;
|
||||
|
||||
@@ -44,6 +44,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, false, "Allow dragging and dropping items to deposit items into this inventory.")]
|
||||
public bool AllowDragAndDrop
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
[Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")]
|
||||
public bool AutoInteractWithContained
|
||||
@@ -166,17 +173,21 @@ namespace Barotrauma.Items.Components
|
||||
public bool CanBeContained(Item item)
|
||||
{
|
||||
if (ContainableItems.Count == 0) { return true; }
|
||||
return (ContainableItems.Find(c => c.MatchesItem(item)) != null);
|
||||
return ContainableItems.Find(c => c.MatchesItem(item)) != null;
|
||||
}
|
||||
public bool CanBeContained(ItemPrefab itemPrefab)
|
||||
{
|
||||
if (ContainableItems.Count == 0) { return true; }
|
||||
return (ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null);
|
||||
return ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (item.body != null &&
|
||||
if (item.ParentInventory is CharacterInventory)
|
||||
{
|
||||
item.SetContainedItemPositions();
|
||||
}
|
||||
else if (item.body != null &&
|
||||
item.body.Enabled &&
|
||||
item.body.FarseerBody.Awake)
|
||||
{
|
||||
@@ -209,22 +220,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
if (SpawnWithId.Length > 0)
|
||||
{
|
||||
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
|
||||
if (prefab != null)
|
||||
{
|
||||
if (Inventory != null && Inventory.Items.Any(it => it == null))
|
||||
{
|
||||
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool HasRequiredItems(Character character, bool addMessage, string msg = null)
|
||||
{
|
||||
return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
|
||||
@@ -284,6 +279,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool Combine(Item item, Character user)
|
||||
{
|
||||
if (!AllowDragAndDrop && user != null) { return false; }
|
||||
|
||||
if (!ContainableItems.Any(x => x.MatchesItem(item))) { return false; }
|
||||
if (user != null && !user.CanAccessInventory(Inventory)) { return false; }
|
||||
|
||||
@@ -354,16 +351,28 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void OnMapLoaded()
|
||||
{
|
||||
if (itemIds == null) { return; }
|
||||
|
||||
for (ushort i = 0; i < itemIds.Length; i++)
|
||||
{
|
||||
if (!(Entity.FindEntityByID(itemIds[i]) is Item item)) { continue; }
|
||||
if (i >= Inventory.Capacity) { continue; }
|
||||
Inventory.TryPutItem(item, i, false, false, null, false);
|
||||
if (itemIds != null)
|
||||
{
|
||||
for (ushort i = 0; i < itemIds.Length; i++)
|
||||
{
|
||||
if (!(Entity.FindEntityByID(itemIds[i]) is Item item)) { continue; }
|
||||
if (i >= Inventory.Capacity) { continue; }
|
||||
Inventory.TryPutItem(item, i, false, false, null, false);
|
||||
}
|
||||
itemIds = null;
|
||||
}
|
||||
|
||||
itemIds = null;
|
||||
if (SpawnWithId.Length > 0)
|
||||
{
|
||||
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
|
||||
if (prefab != null)
|
||||
{
|
||||
if (Inventory != null && Inventory.Items.Any(it => it == null))
|
||||
{
|
||||
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ShallowRemoveComponentSpecific()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class ItemLabel : ItemComponent, IDrawableComponent
|
||||
partial class ItemLabel : ItemComponent, IDrawableComponent, IServerSerializable
|
||||
{
|
||||
public Vector2 DrawSize
|
||||
{
|
||||
@@ -10,12 +11,16 @@ namespace Barotrauma.Items.Components
|
||||
get { return Vector2.Zero; }
|
||||
}
|
||||
|
||||
partial void OnStateChanged();
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
|
||||
{
|
||||
switch (connection.Name)
|
||||
{
|
||||
case "set_text":
|
||||
if (Text == signal) { return; }
|
||||
Text = signal;
|
||||
OnStateChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ namespace Barotrauma.Items.Components
|
||||
if (user == null
|
||||
|| user.Removed
|
||||
|| user.SelectedConstruction != item
|
||||
|| item.ParentInventory != null
|
||||
|| !user.CanInteractWith(item)
|
||||
|| (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
|
||||
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater))
|
||||
@@ -221,7 +222,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
user.AnimController.ResetPullJoints();
|
||||
|
||||
if (dir != 0) user.AnimController.TargetDir = dir;
|
||||
if (dir != 0) { user.AnimController.TargetDir = dir; }
|
||||
|
||||
foreach (LimbPos lb in limbPositions)
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private float maxForce;
|
||||
|
||||
private Attack propellerDamage;
|
||||
private readonly Attack propellerDamage;
|
||||
|
||||
private float damageTimer;
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private float controlLockTimer;
|
||||
|
||||
public Character User;
|
||||
|
||||
[Editable(0.0f, 10000000.0f),
|
||||
Serialize(2000.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")]
|
||||
public float MaxForce
|
||||
@@ -106,6 +108,11 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
//arbitrary multiplier that was added to changes in submarine mass without having to readjust all engines
|
||||
float forceMultiplier = 0.1f;
|
||||
if (User != null)
|
||||
{
|
||||
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
|
||||
}
|
||||
|
||||
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage / MinVoltage, 1.0f);
|
||||
Vector2 currForce = new Vector2(force * maxForce * forceMultiplier * voltageFactor, 0.0f);
|
||||
//less effective when in a bad condition
|
||||
@@ -193,6 +200,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
controlLockTimer = 0.1f;
|
||||
targetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f);
|
||||
User = sender;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user