(965c31410a) Unstable v0.10.4.0

This commit is contained in:
Juan Pablo Arce
2020-07-21 08:57:50 -03:00
parent 4f8bd39789
commit 33d3a41104
546 changed files with 45952 additions and 25762 deletions
@@ -0,0 +1,158 @@
using Microsoft.Xna.Framework;
using NLog.Targets;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class CameraTransition
{
public bool Running
{
get;
private set;
}
public Camera AssignedCamera;
private readonly Alignment? cameraStartPos;
private readonly Alignment? cameraEndPos;
private readonly float? startZoom;
private readonly float? endZoom;
public readonly float Duration;
public readonly bool FadeOut;
private readonly CoroutineHandle updateCoroutine;
private Character prevControlled;
public bool AllowInterrupt = false;
public bool RemoveControlFromCharacter = true;
public CameraTransition(ISpatialEntity targetEntity, Camera cam, Alignment? cameraStartPos, Alignment? cameraEndPos, bool fadeOut = true, float duration = 10.0f, float? startZoom = null, float? endZoom = null)
{
Duration = duration;
FadeOut = fadeOut;
this.cameraStartPos = cameraStartPos;
this.cameraEndPos = cameraEndPos;
this.startZoom = startZoom;
this.endZoom = endZoom;
AssignedCamera = cam;
if (targetEntity == null) { return; }
Running = true;
CoroutineManager.StopCoroutines("CameraTransition");
updateCoroutine = CoroutineManager.StartCoroutine(Update(targetEntity, cam), "CameraTransition");
}
public void Stop()
{
CoroutineManager.StopCoroutines(updateCoroutine);
Running = false;
#if CLIENT
if (FadeOut) { GUI.ScreenOverlayColor = Color.TransparentBlack; }
if (prevControlled != null && !prevControlled.Removed)
{
Character.Controlled = prevControlled;
}
#endif
}
private IEnumerable<object> Update(ISpatialEntity targetEntity, Camera cam)
{
if (targetEntity == null) { yield return CoroutineStatus.Success; }
prevControlled = Character.Controlled;
if (RemoveControlFromCharacter)
{
#if CLIENT
GameMain.LightManager.LosEnabled = false;
#endif
Character.Controlled = null;
}
cam.TargetPos = Vector2.Zero;
float startZoom = this.startZoom ?? cam.Zoom;
float endZoom = this.endZoom ?? 0.5f;
Vector2 initialCameraPos = cam.Position;
Vector2? initialTargetPos = targetEntity?.WorldPosition;
float timer = 0.0f;
while (timer < Duration)
{
if (Screen.Selected != GameMain.GameScreen)
{
yield return new WaitForSeconds(0.1f);
#if CLIENT
if (FadeOut) { GUI.ScreenOverlayColor = Color.TransparentBlack; }
#endif
Running = false;
yield return CoroutineStatus.Success;
}
if (prevControlled != null && prevControlled.Removed)
{
prevControlled = null;
}
#if CLIENT
if (AllowInterrupt && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape))
{
break;
}
#endif
Vector2 minPos = targetEntity.WorldPosition;
Vector2 maxPos = targetEntity.WorldPosition;
if (targetEntity is Submarine sub)
{
minPos = new Vector2(sub.WorldPosition.X - sub.Borders.Width / 2, sub.WorldPosition.Y - sub.Borders.Height / 2);
maxPos = new Vector2(sub.WorldPosition.X + sub.Borders.Width / 2, sub.WorldPosition.Y + sub.Borders.Height / 2);
}
Vector2 startPos = cameraStartPos.HasValue ?
new Vector2(
MathHelper.Lerp(minPos.X, maxPos.X, (cameraStartPos.Value.ToVector2().X + 1.0f) / 2.0f),
MathHelper.Lerp(maxPos.Y, minPos.Y, (cameraStartPos.Value.ToVector2().Y + 1.0f) / 2.0f)) :
initialCameraPos;
if (!cameraStartPos.HasValue && initialTargetPos.HasValue)
{
startPos += targetEntity.WorldPosition - initialTargetPos.Value;
}
Vector2 endPos = cameraEndPos.HasValue ?
new Vector2(
MathHelper.Lerp(minPos.X, maxPos.X, (cameraEndPos.Value.ToVector2().X + 1.0f) / 2.0f),
MathHelper.Lerp(maxPos.Y, minPos.Y, (cameraEndPos.Value.ToVector2().Y + 1.0f) / 2.0f)) :
prevControlled?.WorldPosition ?? targetEntity.WorldPosition;
Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, timer / Duration);
cam.Translate(cameraPos - cam.Position);
#if CLIENT
cam.Zoom = MathHelper.SmoothStep(startZoom, endZoom, timer / Duration);
if (timer / Duration > 0.9f)
{
if (FadeOut) { GUI.ScreenOverlayColor = Color.Lerp(Color.TransparentBlack, Color.Black, ((timer / Duration) - 0.9f) * 10.0f); }
}
#endif
timer += CoroutineManager.UnscaledDeltaTime;
yield return CoroutineStatus.Running;
}
Running = false;
yield return new WaitForSeconds(0.1f);
#if CLIENT
GUI.ScreenOverlayColor = Color.TransparentBlack;
GameMain.LightManager.LosEnabled = true;
#endif
if (prevControlled != null && !prevControlled.Removed)
{
Character.Controlled = prevControlled;
}
yield return CoroutineStatus.Success;
}
}
}
@@ -15,7 +15,7 @@ namespace Barotrauma
public static bool DisableEnemyAI;
/// <summary>
/// Enable the character to attack the outposts and the characters inside them. Disabled by default.
/// Enable the character to attack the outposts and the characters inside them. Disabled by default in normal levels, enabled in outpost levels.
/// </summary>
public bool TargetOutposts;
@@ -96,9 +96,13 @@ namespace Barotrauma
private float avoidTimer;
public bool StayInsideLevel = true;
public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; }
public CharacterParams.TargetParams SelectedTargetingParams { get { return selectedTargetingParams; } }
public bool AttackHumans
{
get
@@ -153,6 +157,8 @@ namespace Barotrauma
var mainElement = prefab.XDocument.Root.IsOverride() ? prefab.XDocument.Root.FirstElement() : prefab.XDocument.Root;
targetMemories = new Dictionary<AITarget, AITargetMemory>();
steeringManager = outsideSteering;
//allow targeting outposts and outpost NPCs in outpost levels
TargetOutposts = Level.Loaded != null && Level.Loaded.Type == LevelData.LevelType.Outpost;
List<XElement> aiElements = new List<XElement>();
List<float> aiCommonness = new List<float>();
@@ -298,9 +304,9 @@ namespace Barotrauma
else
{
CharacterParams.TargetParams targetingParams = null;
UpdateTargets(Character, out targetingParams);
if (!IsLatchedOnSub)
{
UpdateTargets(Character, out targetingParams);
UpdateWallTarget();
}
updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
@@ -1367,7 +1373,7 @@ namespace Barotrauma
}
else if (canAttack && attacker.IsHuman && AIParams.TryGetTarget(attacker.SpeciesName, out CharacterParams.TargetParams targetingParams))
{
if (targetingParams.State == AIState.Aggressive)
if (targetingParams.State == AIState.Aggressive || targetingParams.State == AIState.PassiveAggressive)
{
ChangeTargetState(attacker, AIState.Attack, 100);
}
@@ -1561,7 +1567,7 @@ namespace Barotrauma
{
SelectedAiTarget = null;
wallTarget = null;
LatchOntoAI.DeattachFromBody();
LatchOntoAI.DeattachFromBody(cooldown: 1);
}
else if (SelectedAiTarget?.Entity == wallTarget?.Structure)
{
@@ -1849,6 +1855,28 @@ namespace Barotrauma
if (valueModifier == 0.0f) { continue; }
if (SwarmBehavior != null && SwarmBehavior.Members.Any())
{
// 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)
{
if (otherCharacter == character) { continue; }
if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
if (!IsFriendly(character, otherCharacter)) { continue; }
valueModifier /= 2;
}
}
Vector2 toTarget = aiTarget.WorldPosition - character.WorldPosition;
float dist = toTarget.Length();
@@ -2199,23 +2227,23 @@ namespace Barotrauma
private float returnTimer;
private void SteerInsideLevel(float deltaTime)
{
if (SteeringManager is IndoorsSteeringManager) { return; }
if (SteeringManager is IndoorsSteeringManager || !StayInsideLevel) { return; }
if (Level.Loaded == null) { return; }
Vector2 levelSimSize = ConvertUnits.ToSimUnits(Level.Loaded.Size.X, Level.Loaded.Size.Y);
float returnTime = 3;
if (SimPosition.Y < 0)
Point levelSize = Level.Loaded.Size;
float returnTime = 10;
if (WorldPosition.Y < 0)
{
// Too far down
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitY;
}
if (SimPosition.X < 0)
if (WorldPosition.X < 0)
{
// Too far left
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitX;
}
if (SimPosition.X > levelSimSize.X)
if (WorldPosition.X > levelSize.X)
{
// Too far right
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
@@ -2225,7 +2253,7 @@ namespace Barotrauma
{
returnTimer -= deltaTime;
SteeringManager.Reset();
SteeringManager.SteeringManual(deltaTime, returnDir);
SteeringManager.SteeringManual(deltaTime, returnDir * 2);
}
}
@@ -34,6 +34,37 @@ namespace Barotrauma
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
public readonly List<Item> IgnoredItems = new List<Item>();
private class HullSafety
{
public float safety;
public float timer;
public bool IsStale => timer <= 0;
public HullSafety(float safety)
{
Reset(safety);
}
public void Reset(float safety)
{
this.safety = safety;
// How long before the hull safety is considered stale
timer = 0.5f;
}
/// <summary>
/// Returns true when the safety is stale
/// </summary>
public bool Update(float deltaTime)
{
timer = Math.Max(timer - deltaTime, 0);
return IsStale;
}
}
private readonly Dictionary<Hull, HullSafety> knownHulls = new Dictionary<Hull, HullSafety>();
private SteeringManager outsideSteering, insideSteering;
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
@@ -58,6 +89,10 @@ 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>();
public HumanAIController(Character c) : base(c)
{
if (!c.IsHuman)
@@ -67,7 +102,7 @@ namespace Barotrauma
insideSteering = new IndoorsSteeringManager(this, true, false);
outsideSteering = new SteeringManager(this);
objectiveManager = new AIObjectiveManager(c);
reactTimer = Rand.Range(0f, reactionTime);
reactTimer = GetReactionTime();
sortTimer = Rand.Range(0f, sortObjectiveInterval);
InitProjSpecific();
}
@@ -78,6 +113,12 @@ namespace Barotrauma
if (DisableCrewAI || Character.IsIncapacitated || Character.Removed) { return; }
base.Update(deltaTime);
foreach (var values in knownHulls)
{
HullSafety hullSafety = values.Value;
hullSafety.Update(deltaTime);
}
if (unreachableClearTimer > 0)
{
unreachableClearTimer -= deltaTime;
@@ -123,6 +164,15 @@ 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;
@@ -136,7 +186,15 @@ namespace Barotrauma
{
if (Character.CurrentHull != null)
{
VisibleHulls.ForEach(h => PropagateHullSafety(Character, h));
if (Character.TeamID == Character.TeamType.FriendlyNPC)
{
// Outpost npcs don't inform each other about threads, like crew members do.
VisibleHulls.ForEach(h => RefreshHullSafety(h));
}
else
{
VisibleHulls.ForEach(h => PropagateHullSafety(Character, h));
}
}
if (Character.SpeechImpediment < 100.0f)
{
@@ -147,7 +205,7 @@ namespace Barotrauma
UpdateSpeaking();
}
UnequipUnnecessaryItems();
reactTimer = reactionTime * Rand.Range(0.75f, 1.25f);
reactTimer = GetReactionTime();
}
if (objectiveManager.CurrentObjective == null) { return; }
@@ -170,7 +228,7 @@ namespace Barotrauma
else
{
float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X;
run = Math.Abs(xDiff) > 300;
run = Math.Abs(xDiff) > 500;
}
}
}
@@ -276,127 +334,137 @@ namespace Barotrauma
{
if (!NeedsDivingGear(Character, Character.CurrentHull, out _))
{
bool oxygenLow = Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool shouldKeepTheGearOn = Character.AnimController.HeadInWater
|| Character.CurrentHull.WaterPercentage > 50
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
bool removeDivingSuit = !Character.AnimController.HeadInWater && oxygenLow;
bool takeMaskOff = !Character.AnimController.HeadInWater && oxygenLow;
if (!removeDivingSuit)
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
if (oxygenLow)
{
if (shouldKeepTheGearOn)
{
removeDivingSuit = false;
}
shouldKeepTheGearOn = false;
}
if (!takeMaskOff)
bool removeDivingSuit = !shouldKeepTheGearOn;
bool takeMaskOff = !shouldKeepTheGearOn;
if (!shouldKeepTheGearOn && !oxygenLow)
{
if (shouldKeepTheGearOn)
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
{
takeMaskOff = false;
removeDivingSuit = true;
takeMaskOff = true;
}
}
if (!shouldKeepTheGearOn && (!takeMaskOff || !removeDivingSuit))
{
foreach (var objective in ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(includingSelf: true))
else
{
if (objective is AIObjectiveGoTo gotoObjective)
bool removeSuit = false;
bool removeMask = false;
foreach (var objective in ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(includingSelf: true))
{
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 (objective is AIObjectiveGoTo gotoObjective)
{
removeDivingSuit = false;
takeMaskOff = false;
break;
}
else if (gotoObjective.mimic)
{
if (!removeDivingSuit)
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 _))
{
removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character);
removeDivingSuit = false;
takeMaskOff = false;
break;
}
if (!takeMaskOff)
else if (gotoObjective.mimic)
{
takeMaskOff = !HasDivingMask(gotoObjective.Target as Character);
}
}
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
{
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null)
{
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
divingSuit.Drop(Character);
}
else
{
findItemState = FindItemState.DivingSuit;
if (FindSuitableContainer(divingSuit, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
if (!removeSuit)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character);
if (removeDivingSuit)
{
DropIfFailsToContain = false
};
decontainObjective.Abandoned += () =>
{
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
removeSuit = true;
}
}
else
if (!removeMask)
{
divingSuit.Drop(Character);
takeMaskOff = !HasDivingMask(gotoObjective.Target as Character);
if (takeMaskOff)
{
removeMask = true;
}
}
}
}
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
if (takeMaskOff)
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
{
var mask = Character.Inventory.FindItemByTag("divingmask");
if (mask != null && Character.Inventory.IsInLimbSlot(mask, InvSlotType.Head))
if (removeDivingSuit)
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
var divingSuit = Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null)
{
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
mask.Drop(Character);
divingSuit.Drop(Character);
}
else
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
findItemState = FindItemState.DivingSuit;
if (FindSuitableContainer(divingSuit, 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);
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
{
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);
}
}
}
}
}
}
@@ -404,41 +472,41 @@ namespace Barotrauma
}
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{
if (!ObjectiveManager.CurrentObjective.UnequipItems || !ObjectiveManager.GetActiveObjective().UnequipItems) { return; }
if (ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>()) { return; }
foreach (var item in Character.Inventory.Items)
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{
if (item == null) { continue; }
if (Character.HasEquippedItem(item) &&
(Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand)))
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 (FindSuitableContainer(item, out Item targetContainer))
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
if (FindSuitableContainer(item, out Item targetContainer))
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
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);
}
}
else
{
item.Drop(Character);
findItemState = FindItemState.OtherItem;
}
}
else
{
findItemState = FindItemState.OtherItem;
}
}
}
}
@@ -559,7 +627,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.RepairThreshold)) { continue; }
if (item.Repairables.All(r => item.ConditionPercentage > r.RepairIconThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
@@ -574,7 +642,13 @@ namespace Barotrauma
}
if (newOrder != null)
{
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
if (Character.TeamID == Character.TeamType.FriendlyNPC)
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default,
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
minDurationBetweenSimilar: 60.0f);
}
else if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
#if SERVER
@@ -588,24 +662,40 @@ namespace Barotrauma
{
if (Character.Oxygen < 20.0f)
{
Character.Speak(TextManager.Get("DialogLowOxygen"), null, 0, "lowoxygen", 30.0f);
Character.Speak(TextManager.Get("DialogLowOxygen"), null, Rand.Range(0.5f, 5.0f), "lowoxygen", 30.0f);
}
if (Character.Bleeding > 2.0f)
{
Character.Speak(TextManager.Get("DialogBleeding"), null, 0, "bleeding", 30.0f);
Character.Speak(TextManager.Get("DialogBleeding"), null, Rand.Range(0.5f, 5.0f), "bleeding", 30.0f);
}
if (Character.PressureTimer > 50.0f && Character.CurrentHull != null)
{
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, true), null, 0, "pressure", 30.0f);
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, true), null, Rand.Range(0.5f, 5.0f), "pressure", 30.0f);
}
}
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
float damage = attackResult.Damage;
if (damage <= 0) { return; }
// excluding poisons etc
float realDamage = attackResult.Damage;
// including poisons etc
float totalDamage = realDamage;
foreach (Affliction affliction in attackResult.Afflictions)
{
totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
}
if (totalDamage <= 0) { return; }
if (attacker != null)
{
if (!damageDoneByAttacker.ContainsKey(attacker))
{
damageDoneByAttacker[attacker] = 0.0f;
}
damageDoneByAttacker[attacker] += totalDamage;
attackers.Add(attacker);
}
if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
if (attacker == null || attacker.IsDead || attacker.Removed)
{
@@ -617,6 +707,11 @@ namespace Barotrauma
//if (Character.LastDamageSource == null) { return; }
//AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
}
else if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID))
{
// Don't react on damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player
return;
}
else if (IsFriendly(attacker))
{
if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
@@ -627,62 +722,133 @@ namespace Barotrauma
}
if (attacker.IsBot)
{
// Don't retaliate on damage done by friendly ai, because we know that it's accidental
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
// Don't retaliate on damage done by human ai, because we know it's accidental
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime() * 2);
}
else
{
// If not on the same team, always stay defensive
if (attacker.TeamID != Character.TeamID)
if (Character.IsSecurity)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
// TODO
}
else
{
float dmgPercentage = MathUtils.Percentage(damage, Character.CharacterHealth.Vitality);
if (dmgPercentage < 10)
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)
{
// Don't retaliate on minor (accidental) dmg done by characters that are in the same team
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
if (otherCharacter == Character || otherCharacter.TeamID != Character.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
otherCharacter.TurnedHostileByEvent)
{
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);
}
else if (isWitnessing)
{
// Other witnesses retreat to safety
otherHumanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime());
}
}
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
}
if (attacker.TeamID != Character.TeamID)
{
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 (!Character.IsSecurity)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime() * 2);
}
}
else
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
AddCombatObjective(DetermineCombatMode(Character, dmgThreshold: 20, allowOffensive: false), attacker, GetReactionTime() * 2);
}
}
}
}
else
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive);
AddCombatObjective(DetermineCombatMode(Character), attacker);
}
void AddCombatObjective(AIObjectiveCombat.CombatMode mode, float delay = 0)
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float dmgThreshold = 10, bool allowOffensive = true)
{
bool holdPosition = Character.Info?.Job?.Prefab.Identifier == "watchman";
if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective)
if (!IsFriendly(attacker))
{
if (combatObjective.Enemy != attacker || (combatObjective.Enemy == null && attacker == null))
{
// Replace the old objective with the new.
ObjectiveManager.Objectives.Remove(combatObjective);
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode, objectiveManager) { HoldPosition = holdPosition});
}
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
}
else
{
if (delay > 0)
if (GetDamageDoneByAttacker(attacker) > dmgThreshold)
{
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode, objectiveManager) { HoldPosition = holdPosition }, delay);
return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
}
else
{
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode, objectiveManager) { HoldPosition = holdPosition });
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
}
}
}
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, bool allowHoldFire = false)
{
if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective)
{
// Don't replace offensive mode with something else
if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; }
if (combatObjective.Mode != mode || combatObjective.Enemy != attacker || (combatObjective.Enemy == null && attacker == null))
{
// Replace the old objective with the new.
ObjectiveManager.Objectives.Remove(combatObjective);
ObjectiveManager.AddObjective(CreateCombatObjective());
}
}
else
{
if (delay > 0)
{
ObjectiveManager.AddObjective(CreateCombatObjective(), delay);
}
else
{
ObjectiveManager.AddObjective(CreateCombatObjective());
}
}
AIObjectiveCombat CreateCombatObjective()
{
var objective = new AIObjectiveCombat(Character, attacker, mode, objectiveManager)
{
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
abortCondition = abortCondition,
allowHoldFire = allowHoldFire,
};
if (onAbort != null)
{
objective.Abandoned += onAbort;
}
return objective;
}
}
public void SetOrder(Order order, string option, Character orderGiver, bool speak = true)
{
CurrentOrderOption = option;
@@ -733,7 +899,7 @@ namespace Barotrauma
private void CheckCrouching(float deltaTime)
{
crouchRaycastTimer -= deltaTime;
if (crouchRaycastTimer > 0.0f) return;
if (crouchRaycastTimer > 0.0f) { return; }
crouchRaycastTimer = crouchRaycastInterval;
@@ -743,7 +909,59 @@ namespace Barotrauma
//do a raycast upwards to find any walls
float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall) != null;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return !(fixture.Body.UserData is Submarine); }) != null;
}
public bool AllowCampaignInteraction()
{
if (Character == null || Character.Removed || Character.IsIncapacitated) { return false; }
switch (ObjectiveManager.CurrentObjective)
{
case AIObjectiveCombat _:
case AIObjectiveFindSafety _:
case AIObjectiveExtinguishFires _:
case AIObjectiveFightIntruders _:
case AIObjectiveFixLeaks _:
return false;
}
return true;
}
public bool TryToMoveItem(Item item, Inventory targetInventory, bool dropIfCannotMove = true)
{
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < targetInventory.Items.Length; i++)
{
if (targetInventory is CharacterInventory characterInventory)
{
//slot not needed by the item, continue
if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; }
}
targetSlot = i;
//slot free, continue
var otherItem = targetInventory.Items[i];
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
continue;
}
if (dropIfCannotMove)
{
//if everything else fails, simply drop the existing item
otherItem.Drop(Character);
}
}
}
return targetInventory.TryPutItem(item, targetSlot, false, false, Character);
}
public static bool NeedsDivingGear(Character character, Hull hull, out bool needsSuit)
@@ -769,28 +987,108 @@ 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", "oxygensource", conditionPercentage);
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, "divingsuit", out _, "oxygensource", 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", "oxygensource", conditionPercentage);
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, "divingmask", out _, "oxygensource", conditionPercentage, requireEquipped: true);
public static bool HasItem(Character character, string tagOrIdentifier, string containedTag = null, float conditionPercentage = 0)
public static bool HasItem(Character character, string tagOrIdentifier, out Item item, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false)
{
item = null;
if (character == null) { return false; }
if (character.Inventory == null) { return false; }
var item = character.Inventory.FindItemByIdentifier(tagOrIdentifier) ?? character.Inventory.FindItemByTag(tagOrIdentifier);
item = character.Inventory.FindItemByIdentifier(tagOrIdentifier) ?? character.Inventory.FindItemByTag(tagOrIdentifier);
return item != null &&
item.ConditionPercentage > conditionPercentage &&
character.HasEquippedItem(item) &&
item.ConditionPercentage >= conditionPercentage &&
(!requireEquipped || character.HasEquippedItem(item)) &&
(containedTag == null ||
(item.ContainedItems != null &&
item.ContainedItems.Any(i => i.HasTag(containedTag) && i.ConditionPercentage > conditionPercentage)));
}
public static void ItemTaken(Item item, Character character)
{
if (item == null || character == null || item.GetComponent<LevelResource>() != null) { return; }
Character thief = character;
bool someoneSpoke = false;
if (item.SpawnedInOutpost && thief.TeamID != Character.TeamType.FriendlyNPC && !item.HasTag("handlocker"))
{
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
!otherHumanAI.VisibleHulls.Contains(thief.CurrentHull))
{
continue;
}
//if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
if (!otherCharacter.CanSeeCharacter(thief)) { continue; }
if (!someoneSpoke)
{
if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.Value -= reputationLoss;
}
item.StolenDuringRound = true;
otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f);
someoneSpoke = true;
}
// React if we are security
if (!TriggerSecurity(otherHumanAI))
{
// Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition)))
{
if (TriggerSecurity(security.AIController as HumanAIController))
{
// Only alert one guard at a time
break;
}
}
}
}
}
else if (item.OwnInventory?.FindItem(it => it.SpawnedInOutpost, true) is { } foundItem)
{
ItemTaken(foundItem, character);
}
bool TriggerSecurity(HumanAIController humanAI)
{
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
abortCondition: () => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
onAbort: () =>
{
if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
{
humanAI.ObjectiveManager.AddObjective(new AIObjectiveGetItem(humanAI.Character, item, humanAI.ObjectiveManager, equip: false)
{
BasePriority = 10
});
}
},
allowHoldFire: true);
return true;
}
}
// 0.225 - 0.375
private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
/// <summary>
/// Updates the hull safety for all ai characters in the team.
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threads.
/// The safety levels need to be calculated for each bot individually, because the formula takes into account things like current orders.
/// There's now a cached value per each hull, which should prevent too frequent calculations.
/// </summary>
public static void PropagateHullSafety(Character character, Hull hull)
{
@@ -887,7 +1185,30 @@ namespace Barotrauma
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
}
public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
public float GetDamageDoneByAttacker(Character attacker)
{
if (!damageDoneByAttacker.TryGetValue(attacker, out float dmg))
{
dmg = 0;
}
return dmg;
}
private void StoreHullSafety(Hull hull, HullSafety safety)
{
if (knownHulls.ContainsKey(hull))
{
// Update existing. Shouldn't currently happen, but things might change.
knownHulls[hull] = safety;
}
else
{
// Add new
knownHulls.Add(hull, safety);
}
}
private float CalculateHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
{
bool isCurrentHull = character == Character && character.CurrentHull == hull;
if (hull == null)
@@ -903,12 +1224,11 @@ namespace Barotrauma
// Use the cached visible hulls
visibleHulls = VisibleHulls;
}
// TODO: should we calculate the visible hulls for each hull? -> could be a bit heavy.
bool ignoreFire = objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = GetHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
{
CurrentHullSafety = safety;
@@ -916,7 +1236,7 @@ namespace Barotrauma
return safety;
}
public static float GetHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null) { return 0; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; }
@@ -949,13 +1269,65 @@ namespace Barotrauma
return MathHelper.Clamp(safety * 100, 0, 100);
}
public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
{
if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
{
hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls));
StoreHullSafety(hull, hullSafety);
}
else if (hullSafety.IsStale)
{
hullSafety.Reset(CalculateHullSafety(hull, character, visibleHulls));
}
return hullSafety.safety;
}
public static float GetHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
HullSafety hullSafety;
if (character.AIController is HumanAIController controller)
{
if (!controller.knownHulls.TryGetValue(hull, out hullSafety))
{
hullSafety = new HullSafety(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies));
controller.StoreHullSafety(hull, hullSafety);
}
else if (hullSafety.IsStale)
{
hullSafety.Reset(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies));
}
}
else
{
#if DEBUG
DebugConsole.ThrowError("Cannot store the hull safety, because was unable to cast the AIController as HumanAIController. This should never happen!");
#endif
return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
}
return hullSafety.safety;
}
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
public static bool IsFriendly(Character me, Character other)
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{
bool sameSpecies = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
bool differentTeam = me.TeamID == Character.TeamType.Team1 && other.TeamID == Character.TeamType.Team2 || me.TeamID == Character.TeamType.Team2 && other.TeamID == Character.TeamType.Team1;
return sameSpecies && !differentTeam;
bool sameTeam = me.TeamID == other.TeamID;
// Only enemies are in the Team "None"
bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.None;
bool teamGood = sameTeam || friendlyTeam && !onlySameTeam;
if (!teamGood) { return false; }
bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
if (!speciesGood) { return false; }
if (me.TeamID == Character.TeamType.FriendlyNPC && other.TeamID == Character.TeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
return false;
}
}
return true;
}
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
@@ -101,7 +101,7 @@ namespace Barotrauma
{
currentPath = path;
if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
findPathTimer = 1.0f;
findPathTimer = Math.Min(findPathTimer, 1.0f);
IsPathDirty = false;
}
@@ -138,6 +138,21 @@ namespace Barotrauma
{
return node.Ladders;
}
//if the next node is a hatch, check if the node after that is a ladder
else if (node.ConnectedDoor != null && node.ConnectedDoor.IsHorizontal)
{
index++;
if (currentPath.Nodes.Count > index)
{
node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && !node.Ladders.Item.NonInteractable)
{
return node.Ladders;
}
}
}
}
return null;
}
@@ -246,14 +261,33 @@ namespace Barotrauma
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController;
if (canClimb && !isDiving && IsNextLadderSameAsCurrent)
var ladders = GetNextLadder();
if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item)
{
var ladders = currentPath.CurrentNode.Ladders;
if (character.SelectedConstruction != ladders.Item && ladders.Item.IsInsideTrigger(character.WorldPosition))
if (IsNextNodeLadder || currentPath.CurrentIndex == currentPath.Nodes.Count - 1)
{
currentPath.CurrentNode.Ladders.Item.TryInteract(character, false, true);
if (character.CanInteractWith(ladders.Item))
{
ladders.Item.TryInteract(character, false, true);
}
else
{
// Cannot interact with the current (or next) ladder,
// Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder.
// The intention of this code is to prevent the bots from dropping from the "double ladders".
var previousLadders = currentPath.PrevNode?.Ladders;
if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item &&
character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5)
{
previousLadders.Item.TryInteract(character, false, true);
}
}
}
}
else if (!IsNextLadderSameAsCurrent && character.SelectedConstruction?.GetComponent<Ladder>() != null && character.CanInteractWith(ladders.Item))
{
ladders.Item.TryInteract(character, false, true);
}
}
var collider = character.AnimController.Collider;
if (character.IsClimbing && !isDiving)
{
@@ -420,7 +454,7 @@ namespace Barotrauma
}
else
{
door = currentWaypoint.ConnectedGap.ConnectedDoor;
door = currentWaypoint.ConnectedDoor;
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
@@ -437,7 +471,7 @@ namespace Barotrauma
if (door == null) { return; }
//toggle the door if it's the previous node and open, or if it's current node and closed
if (door.IsOpen != shouldBeOpen)
if ((door.IsOpen || door.IsBroken) != shouldBeOpen)
{
Controller closestButton = null;
float closestDist = 0;
@@ -447,12 +481,12 @@ namespace Barotrauma
// Check that the button is on the right side of the door.
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
int dir = Math.Sign((nextWaypoint ?? currentWaypoint).WorldPosition.X - door.Item.WorldPosition.X);
if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; }
}
else
{
int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
int dir = Math.Sign((nextWaypoint ?? currentWaypoint).WorldPosition.Y - door.Item.WorldPosition.Y);
if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; }
}
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
@@ -585,6 +619,12 @@ namespace Barotrauma
}
}
float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y);
if (node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null)
{
penalty += yDist * 10.0f;
}
return penalty;
}
@@ -305,13 +305,17 @@ namespace Barotrauma
attachJoints.Add(colliderJoint);
}
public void DeattachFromBody()
public void DeattachFromBody(float cooldown = 0)
{
foreach (Joint joint in attachJoints)
{
GameMain.World.Remove(joint);
}
attachJoints.Clear();
attachJoints.Clear();
if (cooldown > 0)
{
attachCooldown = cooldown;
}
}
private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath)
@@ -166,11 +166,33 @@ namespace Barotrauma
private static List<string> GetCurrentFlags(Character speaker)
{
var currentFlags = new List<string>();
if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) currentFlags.Add("SubmarineDeep");
if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) currentFlags.Add("Initial");
if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) { currentFlags.Add("SubmarineDeep"); }
if (GameMain.GameSession != null && Level.Loaded != null)
{
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial"); }
}
else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
{
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0f &&
speaker?.CurrentHull != null &&
speaker.TeamID == Character.TeamType.FriendlyNPC &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
{
currentFlags.Add("EnterOutpost");
}
}
if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f)
{
currentFlags.Add("Casual");
}
}
if (speaker != null)
{
if (speaker.AnimController.InWater) currentFlags.Add("Underwater");
if (speaker.AnimController.InWater) { currentFlags.Add("Underwater"); }
currentFlags.Add(speaker.CurrentHull == null ? "Outside" : "Inside");
if (Character.Controlled != null)
@@ -190,6 +212,15 @@ namespace Barotrauma
currentFlags.Add(currentEffect.DialogFlag);
}
}
if (speaker.TeamID == Character.TeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost)
{
currentFlags.Add("OutpostNPC");
}
if (speaker.CampaignInteractionType != CampaignMode.InteractionType.None)
{
currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType);
}
}
return currentFlags;
@@ -207,7 +238,7 @@ namespace Barotrauma
return lines;
}
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers, List<string> requiredFlags)
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers, IEnumerable<string> requiredFlags)
{
Dictionary<int, Character> assignedSpeakers = new Dictionary<int, Character>();
List<Pair<Character, string>> lines = new List<Pair<Character, string>>();
@@ -215,7 +246,7 @@ namespace Barotrauma
kpv => kpv.Value.Where(conversation => kpv.Key == TextManager.Language && requiredFlags.All(f => conversation.Flags.Contains(f))))).ToList();
if (availableConversations.Count > 0)
{
CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: true);
CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: false);
}
return lines;
}
@@ -229,11 +260,11 @@ namespace Barotrauma
bool ignoreFlags = false)
{
List<NPCConversation> conversations = baseConversation == null ? availableConversations : baseConversation.Responses;
if (conversations.Count == 0) return;
if (conversations.Count == 0) { return; }
int conversationIndex = Rand.Int(conversations.Count);
NPCConversation selectedConversation = conversations[conversationIndex];
if (string.IsNullOrEmpty(selectedConversation.Line)) return;
if (string.IsNullOrEmpty(selectedConversation.Line)) { return; }
Character speaker = null;
//speaker already assigned for this line
@@ -265,8 +296,8 @@ namespace Barotrauma
//select a random line and attempt to find a speaker for it
// and if no valid speaker is found, choose another random line
selectedConversation = GetRandomConversation(potentialLines, baseConversation == null);
if (selectedConversation == null || string.IsNullOrEmpty(selectedConversation.Line)) return;
if (selectedConversation == null || string.IsNullOrEmpty(selectedConversation.Line)) { return; }
//speaker already assigned for this line
if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex))
{
@@ -280,21 +311,24 @@ namespace Barotrauma
if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) ||
selectedConversation.AllowedJobs.Count > 0)
{
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) continue;
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { continue; }
}
//check if the character has all required flags to say the line
if (!ignoreFlags)
{
var characterFlags = GetCurrentFlags(potentialSpeaker);
if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) continue;
if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) { continue; }
}
//check if the character is close enough to hear the rest of the speakers
if (assignedSpeakers.Values.Any(s => !potentialSpeaker.CanHearCharacter(s))) { continue; }
//check if the character has an appropriate personality
if (selectedConversation.allowedSpeakerTags.Count > 0)
{
if (potentialSpeaker.Info?.PersonalityTrait == null) continue;
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) continue;
if (potentialSpeaker.Info?.PersonalityTrait == null) { continue; }
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) { continue; }
}
else
{
@@ -318,7 +352,7 @@ namespace Barotrauma
}
}
if (allowedSpeakers.Count == 0) return;
if (allowedSpeakers.Count == 0) { return; }
speaker = allowedSpeakers[Rand.Int(allowedSpeakers.Count)];
availableSpeakers.Remove(speaker);
assignedSpeakers.Add(selectedConversation.speakerIndex, speaker);
@@ -31,6 +31,7 @@ namespace Barotrauma
public virtual bool KeepDivingGearOn => false;
public virtual bool UnequipItems => false;
public virtual bool AllowOutsideSubmarine => false;
public virtual bool AllowInFriendlySubs => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
@@ -46,6 +47,8 @@ namespace Barotrauma
/// Final priority value after all calculations.
/// </summary>
public float Priority { get; set; }
public float BasePriority { get; set; }
public float PriorityModifier { get; private set; } = 1;
public readonly Character character;
public readonly AIObjectiveManager objectiveManager;
@@ -182,7 +185,18 @@ namespace Barotrauma
}
}
protected bool IsAllowed => AllowOutsideSubmarine || character.Submarine != null && character.Submarine.TeamID == character.TeamID && character.Submarine.Info.IsPlayer;
protected bool IsAllowed
{
get
{
if (AllowOutsideSubmarine) { return true; }
if (character.Submarine == null) { return false; }
return
character.Submarine.TeamID == character.TeamID ||
(AllowInFriendlySubs && character.Submarine.TeamID == Character.TeamType.FriendlyNPC) ||
character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
}
}
/// <summary>
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
@@ -200,7 +214,7 @@ namespace Barotrauma
}
else
{
Priority = CumulatedDevotion;
Priority = BasePriority + CumulatedDevotion;
}
return Priority;
}
@@ -336,7 +350,12 @@ namespace Barotrauma
}
protected set
{
if (isCompleted == value) { return; }
isCompleted = value;
if (isCompleted)
{
OnCompleted();
}
}
}
@@ -346,7 +365,7 @@ namespace Barotrauma
{
hasBeenChecked = true;
CheckSubObjectives();
if (subObjectives.None())
if (subObjectives.None() || ConcurrentObjectives && subObjectives.All(so => so is AIObjectiveGoTo))
{
if (Check())
{
@@ -1,9 +1,9 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -18,13 +18,17 @@ namespace Barotrauma
private readonly CombatMode initialMode;
private float seekWeaponsTimer;
const float seekWeaponsInterval = 1;
private readonly float seekWeaponsInterval = 1;
private float ignoreWeaponTimer;
const float ignoredWeaponsClearTime = 10;
private readonly float ignoredWeaponsClearTime = 10;
const float coolDown = 10.0f;
// Won't take the offensive with weapons that have lower priority than this
const float goodWeaponPriority = 30;
// 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;
private float holdFireTimer;
private bool hasAimed;
private bool isLethalWeapon;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
@@ -37,6 +41,7 @@ namespace Barotrauma
{
_weapon = value;
_weaponComponent = null;
hasAimed = false;
RemoveSubObjective(ref seekAmmunition);
}
}
@@ -73,16 +78,36 @@ namespace Barotrauma
private IEnumerable<FarseerPhysics.Dynamics.Body> myBodies;
private float aimTimer;
private bool canSeeTarget;
private float visibilityCheckTimer;
private readonly float visibilityCheckInterval = 0.2f;
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
public Func<bool> abortCondition;
public bool allowHoldFire;
/// <summary>
/// Don't start using a weapon if this condition is true
/// </summary>
public Func<bool> holdFireCondition;
public enum CombatMode
{
Defensive,
Offensive,
Arrest,
Retreat
}
public CombatMode Mode { get; private set; }
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1)
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool TargetEliminated => Enemy == null || Enemy.Removed || Enemy.IsUnconscious;
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
: base(character, objectiveManager, priorityModifier)
{
Enemy = enemy;
@@ -103,7 +128,16 @@ namespace Barotrauma
public override float GetPriority()
{
Priority = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
if (character.TeamID == Character.TeamType.FriendlyNPC && Enemy != null)
{
if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
{
Priority = 0;
return Priority;
}
}
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, HumanAIController.GetDamageDoneByAttacker(Enemy) / 100.0f);
Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100);
return Priority;
}
@@ -121,43 +155,55 @@ namespace Barotrauma
protected override bool Check()
{
if (initialMode == CombatMode.Offensive && Mode != CombatMode.Offensive)
if (IsOffensiveOrArrest && Mode != initialMode)
{
Abandon = true;
SteeringManager.Reset();
return false;
}
bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0);
if (completed)
{
if (objectiveManager.CurrentOrder == this && Enemy != null && Enemy.IsDead)
{
character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
}
if (Weapon != null)
{
Unequip();
}
}
return completed;
return IsEnemyDisabled || (!IsOffensiveOrArrest && coolDownTimer <= 0);
}
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
protected override void Act(float deltaTime)
{
if (initialMode != CombatMode.Offensive)
if (abortCondition != null && abortCondition())
{
Abandon = true;
SteeringManager.Reset();
return;
}
if (!IsOffensiveOrArrest)
{
coolDownTimer -= deltaTime;
}
if (seekAmmunition == null)
{
if (Mode != CombatMode.Retreat && TryArm() && Enemy != null && !Enemy.Removed)
if (Mode != CombatMode.Retreat && TryArm() && !IsEnemyDisabled)
{
OperateWeapon(deltaTime);
}
if (!HoldPosition && seekAmmunition == null)
if (!HoldPosition)
{
Move(deltaTime);
}
switch (Mode)
{
case CombatMode.Offensive:
if (TargetEliminated && objectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>())
{
// TODO: enable
//character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
}
break;
case CombatMode.Arrest:
if (HumanAIController.HasItem(Enemy, "handlocker", out _, requireEquipped: true))
{
IsCompleted = true;
}
break;
}
}
}
@@ -166,6 +212,7 @@ namespace Barotrauma
switch (Mode)
{
case CombatMode.Offensive:
case CombatMode.Arrest:
Engage();
break;
case CombatMode.Defensive:
@@ -190,7 +237,7 @@ namespace Barotrauma
{
seekWeaponsTimer = seekWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
var allWeapons = GetAllWeapons().ToList();
var allWeapons = GetAllWeapons();
while (allWeapons.Any())
{
Weapon = GetWeapon(allWeapons, out _weaponComponent);
@@ -206,16 +253,6 @@ namespace Barotrauma
Weapon = null;
continue;
}
if (initialMode == CombatMode.Offensive)
{
// In the offensive mode, let's ignore weapons that cannot be used in the offensive mode
if (WeaponComponent.CombatPriority < goodWeaponPriority)
{
allWeapons.Remove(WeaponComponent);
Weapon = null;
continue;
}
}
if (IsLoaded(WeaponComponent))
{
// All good, the weapon is loaded
@@ -253,6 +290,10 @@ namespace Barotrauma
}
}
}
if (Weapon == null)
{
Mode = CombatMode.Retreat;
}
}
else
{
@@ -261,14 +302,6 @@ namespace Barotrauma
Weapon = null;
}
}
if (Weapon == null)
{
Mode = CombatMode.Retreat;
}
else
{
Mode = WeaponComponent.CombatPriority >= goodWeaponPriority ? initialMode : CombatMode.Defensive;
}
return Weapon != null;
bool CheckWeapon(bool seekAmmo)
@@ -296,6 +329,7 @@ namespace Barotrauma
{
case CombatMode.Offensive:
case CombatMode.Defensive:
case CombatMode.Arrest:
if (Equip())
{
Attack(deltaTime);
@@ -308,29 +342,163 @@ namespace Barotrauma
}
}
private Item GetWeapon(out ItemComponent weaponComponent)
{
GetAllWeapons();
return GetWeapon(weapons, out weaponComponent);
}
private Item GetWeapon(out ItemComponent weaponComponent) => GetWeapon(GetAllWeapons(), out weaponComponent);
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
{
weaponComponent = weaponList.OrderByDescending(w => CalculateWeaponPriority(w)).FirstOrDefault();
if (weaponComponent == null) { return null; }
if (weaponComponent.CombatPriority < 1) { return null; }
return weaponComponent.Item;
}
private float CalculateWeaponPriority(ItemComponent weapon)
{
float priority = weapon.CombatPriority;
// Halve the priority for weapons that don't have proper ammunition loaded.
if (!weapon.HasRequiredContainedItems(character, addMessage: false))
weaponComponent = null;
float bestPriority = 0;
float lethalDmg = -1;
foreach (var weapon in weaponList)
{
priority /= 2;
// 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())
{
// Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo).
continue;
}
else
{
// Halve the priority for weapons that don't have proper ammunition loaded.
priority /= 2;
}
}
if (Enemy.Stun > 1)
{
// Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float max = lethalDmg + 1;
if (weapon.Item.HasTag("stunner"))
{
priority = max;
}
else
{
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max);
}
}
}
else if (Mode == CombatMode.Arrest)
{
// Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons.
if (weapon.Item.HasTag("stunner"))
{
priority *= 2;
}
else
{
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
if (diff < 0)
{
priority /= 2;
}
}
}
}
if (priority > bestPriority)
{
weaponComponent = weapon;
bestPriority = priority;
}
}
if (weaponComponent == null) { return null; }
if (bestPriority < 1) { return null; }
if (Mode == CombatMode.Arrest)
{
if (weaponComponent.Item.HasTag("stunner"))
{
isLethalWeapon = false;
}
else
{
if (lethalDmg < 0)
{
lethalDmg = GetLethalDamage(weaponComponent);
}
isLethalWeapon = lethalDmg > 1;
}
if (allowHoldFire && !hasAimed && holdFireTimer <= 0)
{
holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f);
}
}
return weaponComponent.Item;
bool EnemyIsClose() => character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
{
attack = meleeWeapon.Attack;
}
else if (weapon is RangedWeapon rangedWeapon)
{
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
}
return attack;
}
float GetLethalDamage(ItemComponent weapon)
{
float lethalDmg = 0;
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
}
return lethalDmg;
}
float ApproximateStunDamage(ItemComponent weapon, Attack attack)
{
// Try to reduce the priority using the actual damage values and status effects.
// This is an approximation, because we can't check the status effect conditions here.
// The result might be incorrect if there is a high stun effect that's only applied in certain conditions.
var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character));
if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List<StatusEffect> hitEffects))
{
statusEffects = statusEffects.Concat(hitEffects);
}
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == "stun" ? a.Strength : 0);
float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se =>
{
float stunAmount = 0;
var stunAffliction = se.Afflictions.Find(a => a.Identifier == "stun");
if (stunAffliction != null)
{
stunAmount = stunAffliction.Strength;
}
return stunAmount;
});
return attack.Stun + afflictionsStun + effectsStun;
}
return priority;
}
private HashSet<ItemComponent> GetAllWeapons()
@@ -354,30 +522,9 @@ namespace Barotrauma
if (item == null) { return; }
foreach (var component in item.Components)
{
if (component is RangedWeapon rw)
if (component.CombatPriority > 0)
{
weaponList.Add(rw);
}
else if (component is MeleeWeapon mw)
{
weaponList.Add(mw);
}
else
{
var effects = component.statusEffectLists;
if (effects != null)
{
foreach (var statusEffects in effects.Values)
{
foreach (var statusEffect in statusEffects)
{
if (statusEffect.Afflictions.Any())
{
weaponList.Add(component);
}
}
}
}
weaponList.Add(component);
}
}
}
@@ -423,11 +570,11 @@ namespace Barotrauma
private void Retreat(float deltaTime)
{
RemoveSubObjective(ref followTargetObjective);
RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunition);
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
{
retreatObjective = null;
RemoveSubObjective(ref retreatObjective);
}
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
{
@@ -437,7 +584,7 @@ namespace Barotrauma
}
else
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls);
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC);
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
@@ -454,7 +601,6 @@ namespace Barotrauma
}
else
{
// else abandon and fall back to find safety mode
Abandon = true;
}
@@ -476,10 +622,10 @@ namespace Barotrauma
RemoveSubObjective(ref seekAmmunition);
if (followTargetObjective != null && followTargetObjective.Target != Enemy)
{
followTargetObjective = null;
RemoveFollowTarget();
}
TryAddSubObjective(ref followTargetObjective,
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true)
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true, closeEnough: 50)
{
IgnoreIfTargetDead = true,
DialogueIdentifier = "dialogcannotreachtarget",
@@ -490,7 +636,30 @@ namespace Barotrauma
Abandon = true;
SteeringManager.Reset();
});
if (followTargetObjective != null)
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.Stun > 2)
{
if (HumanAIController.HasItem(character, "handlocker", out Item handCuffs))
{
if (!arrestingRegistered)
{
arrestingRegistered = true;
followTargetObjective.Completed += OnArrestTargetReached;
}
followTargetObjective.CloseEnough = 100;
}
else
{
RemoveFollowTarget();
SteeringManager.Reset();
}
}
else if (WeaponComponent == null)
{
RemoveFollowTarget();
SteeringManager.Reset();
}
else
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 1000 :
@@ -499,6 +668,48 @@ namespace Barotrauma
}
}
private bool arrestingRegistered;
private void RemoveFollowTarget()
{
if (arrestingRegistered)
{
followTargetObjective.Completed -= OnArrestTargetReached;
}
RemoveSubObjective(ref followTargetObjective);
arrestingRegistered = false;
}
private void OnArrestTargetReached()
{
if (HumanAIController.HasItem(character, "handlocker", out Item handCuffs) && Enemy.Stun > 0 && character.CanInteractWith(Enemy))
{
if (HumanAIController.TryToMoveItem(handCuffs, Enemy.Inventory))
{
handCuffs.Equip(Enemy);
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
#endif
}
// Confiscate stolen goods.
foreach (var item in Enemy.Inventory.Items)
{
if (item == null || item == handCuffs) { continue; }
if (item.StolenDuringRound)
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, new List<InvSlotType>() { InvSlotType.Any });
}
}
// TODO: enable
//character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f);
IsCompleted = true;
}
}
/// <summary>
/// Seeks for more ammunition. Creates a new subobjective.
/// </summary>
@@ -506,7 +717,7 @@ namespace Barotrauma
{
retreatTarget = null;
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref followTargetObjective);
RemoveFollowTarget();
TryAddSubObjective(ref seekAmmunition,
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
{
@@ -588,7 +799,7 @@ namespace Barotrauma
{
return true;
}
else if (ammunition == null && !HoldPosition && initialMode == CombatMode.Offensive && seekAmmo && ammunitionIdentifiers != null)
else if (ammunition == null && !HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{
SeekAmmunition(ammunitionIdentifiers);
}
@@ -598,46 +809,64 @@ namespace Barotrauma
private void Attack(float deltaTime)
{
character.CursorPosition = Enemy.Position;
if (!character.CanSeeCharacter(Enemy)) { return; }
visibilityCheckTimer -= deltaTime;
if (visibilityCheckTimer <= 0.0f)
{
canSeeTarget = character.CanSeeTarget(Enemy);
visibilityCheckTimer = visibilityCheckInterval;
}
if (!canSeeTarget) { return; }
if (Weapon.RequireAimToUse)
{
bool isOperatingButtons = false;
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen && !door.IsBroken)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
}
if (!isOperatingButtons)
{
character.SetInput(InputType.Aim, false, true);
}
character.SetInput(InputType.Aim, false, true);
}
bool isFacing = character.AnimController.Dir > 0 && Enemy.WorldPosition.X > character.WorldPosition.X || character.AnimController.Dir < 0 && Enemy.WorldPosition.X < character.WorldPosition.X;
if (!isFacing)
hasAimed = true;
if (holdFireTimer > 0)
{
aimTimer = Rand.Range(1f, 1.5f);
holdFireTimer -= deltaTime;
return;
}
if (aimTimer > 0)
{
aimTimer -= deltaTime;
return;
}
if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 0) { 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)
{
if (Vector2.DistanceSquared(character.Position, Enemy.Position) <= meleeWeapon.Range * meleeWeapon.Range)
float sqrRange = meleeWeapon.Range * meleeWeapon.Range;
if (character.AnimController.InWater)
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
if (sqrDist > sqrRange) { return; }
}
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; }
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)
{
// 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);
}
else
{
if (WeaponComponent is RepairTool repairTool)
{
if (Vector2.DistanceSquared(character.Position, Enemy.Position) > repairTool.Range * repairTool.Range) { return; }
if (sqrDist > repairTool.Range * repairTool.Range) { return; }
}
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4)
{
@@ -645,7 +874,6 @@ namespace Barotrauma
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories);
if (pickedBody != null)
@@ -679,6 +907,15 @@ namespace Barotrauma
}
}
protected override void OnCompleted()
{
base.OnCompleted();
if (Weapon != null)
{
Unequip();
}
}
//private float CalculateEnemyStrength()
//{
// float enemyStrength = 0;
@@ -16,6 +16,9 @@ namespace Barotrauma
public string[] ignoredContainerIdentifiers;
public bool checkInventory = true;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
private bool spawnItemIfNotFound = false;
//can either be a tag or an identifier
public readonly string[] itemIdentifiers;
public readonly ItemContainer container;
@@ -38,13 +41,14 @@ namespace Barotrauma
this.item = item;
}
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { }
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier, spawnItemIfNotFound) { }
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
this.spawnItemIfNotFound = spawnItemIfNotFound;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
@@ -147,7 +151,7 @@ namespace Barotrauma
{
// No matching items in the inventory, try to get an item
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory)
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory, spawnItemIfNotFound: spawnItemIfNotFound)
{
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -8,6 +9,7 @@ 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) { }
@@ -21,8 +23,25 @@ namespace Barotrauma
return 100;
}
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
protected override AIObjective ObjectiveConstructor(Character target)
{
var combatObjective = new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
if (character.TeamID == Character.TeamType.FriendlyNPC && target.TeamID == Character.TeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
combatObjective.holdFireCondition = () =>
{
//hold fire while the enemy is in the airlock (except if they've attacked us)
if (HumanAIController.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t.Equals("airlock", System.StringComparison.OrdinalIgnoreCase));
};
character.Speak(TextManager.Get("dialogenteroutpostwarning"), null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning", 30.0f);
}
}
return combatObjective;
}
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveFightIntruders, Character>(character, target);
@@ -18,7 +18,7 @@ namespace Barotrauma
public static float lowOxygenThreshold = 10;
protected override bool Check() => HumanAIController.HasItem(character, gearTag, "oxygensource") || HumanAIController.HasItem(character, fallbackTag, "oxygensource");
protected override bool Check() => HumanAIController.HasItem(character, gearTag, out _, "oxygensource", requireEquipped: true) || HumanAIController.HasItem(character, fallbackTag, out _, "oxygensource", requireEquipped: true);
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -140,7 +141,7 @@ namespace Barotrauma
{
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = FindBestHull();
currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC);
if (currentSafeHull == null)
{
currentSafeHull = previousSafeHull;
@@ -233,12 +234,30 @@ namespace Barotrauma
public Hull FindBestHull(IEnumerable<Hull> ignoredHulls = null, bool allowChangingTheSubmarine = true)
{
//sort the hulls based on distance and which sub they're in
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
//path calculations, only to discard all of them when going through the hulls in the outpost)
float EstimateHullSuitability(Hull hull)
{
float dist =
Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) +
Math.Abs(hull.WorldPosition.Y - character.WorldPosition.Y) * 3;
float suitability = -dist;
if (hull.Submarine != character.Submarine)
{
suitability -= 10000.0f;
}
return suitability;
}
Hull bestHull = null;
float bestValue = 0;
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.hullList.OrderByDescending(h => EstimateHullSuitability(h)))
{
if (hull.Submarine == null) { continue; }
if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; }
if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; }
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
float hullSafety = 0;
@@ -255,6 +274,11 @@ namespace Barotrauma
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
if (hullSafety < bestValue) { continue; }
//avoid airlock modules if not allowed to change the sub
if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t.Equals("airlock", StringComparison.OrdinalIgnoreCase)))
{
continue;
}
// Don't allow to go outside if not already outside.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
@@ -61,7 +61,7 @@ namespace Barotrauma
var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true);
if (weldingTool == null)
{
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, true),
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getWeldingTool));
return;
@@ -88,7 +88,7 @@ namespace Barotrauma
}
if (containedItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager),
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref refuelObjective));
return;
@@ -130,7 +130,8 @@ namespace Barotrauma
{
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager)
{
AllowGoingOutside = !Leak.IsRoomToRoom && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() && HumanAIController.HasDivingSuit(character, conditionPercentage: 50),
// 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
@@ -22,7 +22,11 @@ namespace Barotrauma
private string[] itemIdentifiers;
public IEnumerable<string> Identifiers => itemIdentifiers;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
private bool spawnItemIfNotFound = false;
private Item targetItem;
private Item originalTarget;
private ISpatialEntity moveToTarget;
private bool isDoneSeeking;
public Item TargetItem => targetItem;
@@ -41,19 +45,21 @@ namespace Barotrauma
{
currSearchIndex = -1;
this.equip = equip;
originalTarget = targetItem;
this.targetItem = targetItem;
moveToTarget = targetItem?.GetRootInventoryOwner();
}
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, objectiveManager, equip, checkInventory, priorityModifier) { }
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[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, 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.spawnItemIfNotFound = spawnItemIfNotFound;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
@@ -109,6 +115,14 @@ namespace Barotrauma
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
#endif
Abandon = true;
return;
}
else if (isDoneSeeking && moveToTarget == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red);
#endif
Abandon = true;
return;
@@ -118,8 +132,15 @@ namespace Barotrauma
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Found an item, but it's already equipped by someone else.", Color.Yellow);
#endif
// Try again
Reset();
if (originalTarget == null)
{
// Try again
Reset();
}
else
{
Abandon = true;
}
return;
}
bool canInteract = false;
@@ -155,30 +176,7 @@ namespace Barotrauma
if (equip)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
//slot not needed by the item, continue
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; }
targetSlot = i;
//slot free, continue
var otherItem = character.Inventory.Items[i];
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) &&
character.Inventory.TryPutItem(otherItem, character, new List<InvSlotType>() { InvSlotType.Any }))
{
continue;
}
//if everything else fails, simply drop the existing item
otherItem.Drop(character);
}
}
if (character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character))
if (HumanAIController.TryToMoveItem(targetItem, character.Inventory))
{
targetItem.Equip(character);
IsCompleted = true;
@@ -193,7 +191,7 @@ namespace Barotrauma
}
else
{
if (character.Inventory.TryPutItem(targetItem, null, new List<InvSlotType>() { InvSlotType.Any }))
if (character.Inventory.TryPutItem(targetItem, character, new List<InvSlotType>() { InvSlotType.Any }))
{
IsCompleted = true;
}
@@ -283,10 +281,34 @@ namespace Barotrauma
isDoneSeeking = true;
if (targetItem == null)
{
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 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(", ", itemIdentifiers)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
Abandon = true;
}
else
{
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
{
targetItem = spawnedItem;
if (character.TeamID == Character.TeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInOutpost = true;
}
});
}
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow);
#endif
Abandon = true;
}
}
}
}
@@ -323,8 +345,8 @@ namespace Barotrauma
{
base.Reset();
RemoveSubObjective(ref goToObjective);
targetItem = null;
moveToTarget = null;
targetItem = originalTarget;
moveToTarget = targetItem?.GetRootInventoryOwner();
isDoneSeeking = false;
currSearchIndex = 0;
}
@@ -25,10 +25,13 @@ namespace Barotrauma
public Func<bool> abortCondition;
public Func<PathNode, bool> endNodeFilter;
public Func<float> priorityGetter;
public bool followControlledCharacter;
public bool mimic;
private float _closeEnough = 50;
private readonly float minDistance = 25;
/// <summary>
/// Display units
/// </summary>
@@ -37,7 +40,7 @@ namespace Barotrauma
get { return _closeEnough; }
set
{
_closeEnough = Math.Max(_closeEnough, value);
_closeEnough = Math.Max(minDistance, value);
}
}
public bool IgnoreIfTargetDead { get; set; }
@@ -52,6 +55,8 @@ namespace Barotrauma
public ISpatialEntity Target { get; private set; }
public float? OverridePriority = null;
public override float GetPriority()
{
if (followControlledCharacter && Character.Controlled == null)
@@ -68,7 +73,18 @@ namespace Barotrauma
}
else
{
Priority = objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : 10;
if (priorityGetter != null)
{
Priority = priorityGetter();
}
else if (OverridePriority.HasValue)
{
Priority = OverridePriority.Value;
}
else
{
Priority = objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : 10;
}
}
return Priority;
}
@@ -86,7 +102,8 @@ namespace Barotrauma
}
else if (Target is Character)
{
CloseEnough = Math.Max(closeEnough, AIObjectiveGetItem.DefaultReach);
//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);
}
else
{
@@ -289,7 +306,7 @@ namespace Barotrauma
return null;
}
private bool IsCloseEnough
public bool IsCloseEnough
{
get
{
@@ -1,4 +1,5 @@
using FarseerPhysics;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -12,12 +13,48 @@ namespace Barotrauma
public override bool UnequipItems => true;
public override bool AllowOutsideSubmarine => true;
private readonly float newTargetIntervalMin = 10;
private readonly float newTargetIntervalMax = 20;
private readonly float standStillMin = 2;
private readonly float standStillMax = 10;
private readonly float walkDurationMin = 5;
private readonly float walkDurationMax = 10;
private BehaviorType behavior;
public BehaviorType Behavior
{
get { return behavior; }
set
{
behavior = value;
switch (behavior)
{
case BehaviorType.Active:
newTargetIntervalMin = 10;
newTargetIntervalMax = 20;
standStillMin = 2;
standStillMax = 10;
walkDurationMin = 5;
walkDurationMax = 10;
break;
case BehaviorType.Passive:
newTargetIntervalMin = 60;
newTargetIntervalMax = 120;
standStillMin = 30;
standStillMax = 60;
walkDurationMin = 5;
walkDurationMax = 10;
break;
}
}
}
private float newTargetIntervalMin;
private float newTargetIntervalMax;
private float standStillMin;
private float standStillMax;
private float walkDurationMin;
private float walkDurationMax;
public enum BehaviorType
{
Active,
Passive,
StayInHull
}
private Hull currentTarget;
private float newTargetTimer;
@@ -27,13 +64,20 @@ namespace Barotrauma
private float standStillTimer;
private float walkDuration;
private Character tooCloseCharacter;
const float chairCheckInterval = 5.0f;
private float chairCheckTimer;
private readonly List<Hull> targetHulls = new List<Hull>(20);
private readonly List<float> hullWeights = new List<float>(20);
public AIObjectiveIdle(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
Behavior = BehaviorType.Passive;
standStillTimer = Rand.Range(-10.0f, 10.0f);
walkDuration = Rand.Range(0.0f, 10.0f);
chairCheckTimer = Rand.Range(0.0f, chairCheckInterval);
CalculatePriority();
}
@@ -42,9 +86,12 @@ namespace Barotrauma
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); }
private float randomTimer;
private float randomUpdateInterval = 5;
public float Random { get; private set; }
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)
{
@@ -73,6 +120,31 @@ namespace Barotrauma
//}
}
private float timerMargin;
private void SetTargetTimerLow()
{
// Increases the margin each time the method is called -> takes longer between the path finding calls.
// The intention behind this is to reduce unnecessary path finding calls in cases where the bot can't find a path.
timerMargin += 0.5f;
timerMargin = Math.Min(timerMargin, newTargetIntervalMin);
newTargetTimer = Math.Min(newTargetTimer, timerMargin);
}
private void SetTargetTimerHigh()
{
// This method is used to the timer between the current value and the min so that it never reaches 0.
// Prevents pathfinder calls.
newTargetTimer = Math.Max(newTargetTimer, newTargetIntervalMin);
timerMargin = 0;
}
private void SetTargetTimerNormal()
{
newTargetTimer = currentTarget != null && character.AnimController.InWater ? newTargetIntervalMin : Rand.Range(newTargetIntervalMin, newTargetIntervalMax);
timerMargin = 0;
}
protected override void Act(float deltaTime)
{
if (PathSteering == null) { return; }
@@ -82,97 +154,95 @@ namespace Barotrauma
{
character.DeselectCharacter();
}
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
}
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior != BehaviorType.StayInHull)
{
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (currentTargetIsInvalid || currentTarget == null && HumanAIController.VisibleHulls.Any(h => IsForbidden(h)))
{
//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
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
//standStillTimer = 0.0f;
}
else if (character.IsClimbing)
{
if (currentTarget == null)
bool IsSteeringFinished() => PathSteering.CurrentPath != null && PathSteering.CurrentPath.Finished;
if (currentTargetIsInvalid || currentTarget == null || IsSteeringFinished() && (IsForbidden(character.CurrentHull) || IsInWrongSub()))
{
newTargetTimer = 0;
//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 (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
else if (character.IsClimbing)
{
// Don't allow new targets when climbing straight up or down
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
}
}
else if (character.AnimController.InWater)
{
if (currentTarget == null)
{
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
}
}
if (newTargetTimer <= 0.0f)
{
if (!searchingNewHull)
{
//find all available hulls first
FindTargetHulls();
searchingNewHull = true;
return;
}
else if (targetHulls.Count > 0)
{
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullAllowed = !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node =>
if (currentTarget == null)
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe or forbidden hulls on the way to the target
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; }
return true;
});
if (path.Unreachable)
SetTargetTimerLow();
}
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
{
//can't go to this room, remove it from the list and try another room next frame
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
// Don't allow new targets when climbing straight up or down
SetTargetTimerHigh();
}
}
else if (character.AnimController.InWater)
{
if (currentTarget == null)
{
SetTargetTimerLow();
}
}
if (newTargetTimer <= 0.0f)
{
if (!searchingNewHull)
{
//find all available hulls first
FindTargetHulls();
searchingNewHull = true;
return;
}
searchingNewHull = false;
}
else
{
// Couldn't find a target for some reason -> reset
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
searchingNewHull = false;
}
else if (targetHulls.Count > 0)
{
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
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; }
// Check that there is no unsafe or forbidden hulls on the way to the target
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; }
return true;
});
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room next frame
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
return;
}
searchingNewHull = false;
}
else
{
// Couldn't find a target for some reason -> reset
SetTargetTimerHigh();
searchingNewHull = false;
}
if (currentTarget != null)
{
character.AIController.SelectTarget(currentTarget.AiTarget);
string errorMsg = null;
#if DEBUG
bool isRoomNameFound = currentTarget.DisplayName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")";
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null);
PathSteering.SetPath(path);
}
newTargetTimer = currentTarget != null && character.AnimController.InWater ? newTargetIntervalMin : Rand.Range(newTargetIntervalMin, newTargetIntervalMax);
if (currentTarget != null)
{
character.AIController.SelectTarget(currentTarget.AiTarget);
string errorMsg = null;
#if DEBUG
bool isRoomNameFound = currentTarget.DisplayName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")";
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null);
PathSteering.SetPath(path);
}
SetTargetTimerNormal();
}
newTargetTimer -= deltaTime;
}
newTargetTimer -= deltaTime;
//wander randomly
// - if reached the end of the path
@@ -180,12 +250,13 @@ namespace Barotrauma
// - if the path requires going outside
if (!character.IsClimbing)
{
if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
if (behavior == BehaviorType.StayInHull || SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
(PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
{
Wander(deltaTime);
return;
}
character.SelectedConstruction = null;
}
if (currentTarget != null)
@@ -214,7 +285,58 @@ namespace Barotrauma
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
PathSteering.Reset();
if (character.CurrentHull != null && character.CurrentHull.Rect.Width > 150 && tooCloseCharacter == null)
{
foreach (Character c in Character.CharacterList)
{
if (c == character || !c.IsBot || c.CurrentHull != character.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))
{
//if there are characters too close on both sides, don't try to steer away from them
//because it'll cause the character to spaz out trying to avoid both
if (tooCloseCharacter != null &&
Math.Sign(tooCloseCharacter.WorldPosition.X - character.WorldPosition.X) != Math.Sign(c.WorldPosition.X - character.WorldPosition.X))
{
tooCloseCharacter = null;
break;
}
tooCloseCharacter = c;
}
HumanAIController.FaceTarget(c);
}
}
if (tooCloseCharacter != null && !tooCloseCharacter.Removed && Vector2.DistanceSquared(tooCloseCharacter.WorldPosition, character.WorldPosition) < 50.0f * 50.0f)
{
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));
return;
}
else
{
PathSteering.Reset();
tooCloseCharacter = null;
}
chairCheckTimer -= deltaTime;
if (chairCheckTimer <= 0.0f && character.SelectedConstruction == null)
{
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != character.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)
@@ -222,6 +344,7 @@ namespace Barotrauma
standStillTimer = Rand.Range(standStillMin, standStillMax);
}
}
PathSteering.Wander(deltaTime);
}
@@ -234,12 +357,27 @@ namespace Barotrauma
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
if (hull.Submarine == null) { continue; }
if (character.Submarine == null) { break; }
if (hull.Submarine.TeamID != character.Submarine.TeamID) { continue; }
if (hull.Submarine.Info.Type != character.Submarine.Info.Type) { continue; }
// If the character is inside, only take connected subs into account.
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; }
if (character.TeamID == Character.TeamType.FriendlyNPC)
{
if (hull.Submarine.TeamID != character.TeamID)
{
// Don't allow npcs to idle in a sub that's not in their team (like the player sub)
continue;
}
}
else
{
if (hull.Submarine.TeamID != character.Submarine.TeamID)
{
// Don't allow to idle in the subs that are not in the same team as the current sub
// -> the crew ai bots can't change the sub from outpost to main sub or vice versa on their own
continue;
}
}
if (IsForbidden(hull)) { continue; }
// Ignore hulls that are too low to stand inside
// Check that the hull is linked
if (!character.Submarine.GetConnectedSubs().Contains(hull.Submarine)) { continue; }
// Ignore hulls that are too low to stand inside.
if (character.AnimController is HumanoidAnimController animController)
{
if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
@@ -260,6 +398,17 @@ namespace Barotrauma
hullWeights.Add(weight);
}
}
if (PreferredOutpostModuleTypes.Any() && character.CurrentHull != null)
{
for (int i = 0; i < targetHulls.Count; i++)
{
if (targetHulls[i].OutpostModuleTags.Any(t => PreferredOutpostModuleTypes.Contains(t)))
{
hullWeights[i] *= Rand.Range(10.0f, 100.0f);
}
}
}
}
public static bool IsForbidden(Hull hull)
@@ -23,6 +23,8 @@ namespace Barotrauma
private readonly Character character;
public HumanAIController HumanAIController => character.AIController as HumanAIController;
private float _waitTimer;
/// <summary>
@@ -123,7 +125,7 @@ namespace Barotrauma
{
var orderPrefab = Order.GetPrefab(autonomousObjective.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); }
var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, false)?.GetRandom() : null;
var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID)?.GetRandom() : null;
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character);
if (order == null) { continue; }
@@ -298,7 +300,7 @@ namespace Barotrauma
if (orderGiver == null) { return null; }
newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
CloseEnough = 100,
CloseEnough = Rand.Range(90, 100) + Rand.Range(50, 70) * Math.Min(HumanAIController.CountCrew(c => c.ObjectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.Target == orderGiver, onlyBots: true), 4),
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
followControlledCharacter = orderGiver == character,
@@ -81,11 +81,11 @@ namespace Barotrauma
break;
}
}
if (targetItem.CurrentHull == null || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(target, out _))
{
Priority = 0;
}
else if (Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
if (targetItem.CurrentHull == null ||
targetItem.Submarine != character.Submarine && objectiveManager.CurrentOrder != this ||
targetItem.CurrentHull.FireSources.Any() ||
HumanAIController.IsItemOperatedByAnother(target, out _) ||
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
Priority = 0;
}
@@ -111,10 +111,10 @@ namespace Barotrauma
var target = GetTarget();
if (target == null)
{
Abandon = true;
#if DEBUG
throw new Exception("target null");
#endif
Abandon = true;
}
else if (target.Item.NonInteractable)
{
@@ -51,7 +51,7 @@ namespace Barotrauma
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
}
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character);
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);
@@ -148,7 +148,8 @@ namespace Barotrauma
{
if (character.SelectedConstruction != Item)
{
if (!Item.TryInteract(character, true, true))
if (!Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) &&
!Item.TryInteract(character, ignoreRequiredItems: true, forceActionKey: true))
{
Abandon = true;
}
@@ -25,6 +25,8 @@ namespace Barotrauma
public override bool AllowMultipleInstances => true;
public readonly static float RequiredSuccessFactor = 0.4f;
public override bool IsDuplicate<T>(T otherObjective) =>
(otherObjective as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
@@ -110,7 +112,7 @@ namespace Barotrauma
}
if (RequireAdequateSkills)
{
return Targets.Sum(t => GetTargetPriority(t, character)) * ratio;
return Targets.Sum(t => GetTargetPriority(t, character, RequiredSuccessFactor)) * ratio;
}
else
{
@@ -119,10 +121,14 @@ namespace Barotrauma
}
}
public static float GetTargetPriority(Item item, Character character)
public static float GetTargetPriority(Item item, Character character, float requiredSuccessFactor = 0)
{
float damagePriority = MathHelper.Lerp(1, 0, item.Condition / item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, item.Repairables.Average(r => r.DegreeOfSuccess(character)));
if (successFactor < requiredSuccessFactor)
{
return 0;
}
return MathHelper.Lerp(0, 100, MathHelper.Clamp(damagePriority * successFactor, 0, 1));
}
@@ -12,6 +12,8 @@ namespace Barotrauma
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowOutsideSubmarine => true;
const float TreatmentDelay = 0.5f;
const float CloseEnoughToTreat = 100.0f;
@@ -216,50 +218,53 @@ namespace Barotrauma
}
}
}
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
//didn't have any suitable treatments available, try to find some medical items
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
// Find treatments outside of own inventory only if inside the own sub.
if (character.Submarine != null && character.Submarine.TeamID == character.TeamID)
{
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<string, float> treatmentSuitability in currentTreatmentSuitabilities)
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
//didn't have any suitable treatments available, try to find some medical items
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
{
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab)
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<string, float> treatmentSuitability in currentTreatmentSuitabilities)
{
if (!Item.ItemList.Any(it => it.prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(treatmentSuitability.Key);
//only list the first 4 items
if (itemNameList.Count < 4)
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab)
{
itemNameList.Add(itemPrefab.Name);
if (!Item.ItemList.Any(it => it.prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(treatmentSuitability.Key);
//only list the first 4 items
if (itemNameList.Count < 4)
{
itemNameList.Add(itemPrefab.Name);
}
}
}
}
if (itemNameList.Count > 0)
{
string itemListStr = "";
if (itemNameList.Count == 1)
if (itemNameList.Count > 0)
{
itemListStr = itemNameList[0];
string itemListStr = "";
if (itemNameList.Count == 1)
{
itemListStr = itemNameList[0];
}
else
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
}
if (targetCharacter != character)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
}
character.DeselectCharacter();
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
onCompleted: () => RemoveSubObjective(ref getItemObjective),
onAbandon: () => RemoveSubObjective(ref getItemObjective));
}
else
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
}
if (targetCharacter != character)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
}
character.DeselectCharacter();
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true),
onCompleted: () => RemoveSubObjective(ref getItemObjective),
onAbandon: () => RemoveSubObjective(ref getItemObjective));
}
}
if (character != targetCharacter)
@@ -9,9 +9,10 @@ namespace Barotrauma
public override string DebugTag => "rescue all";
public override bool ForceRun => true;
public override bool InverseTargetEvaluation => true;
public override bool AllowOutsideSubmarine => true;
private const float vitalityThreshold = 80;
private const float vitalityThresholdForOrders = 100;
private const float vitalityThreshold = 75;
private const float vitalityThresholdForOrders = 85;
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
{
if (manager == null)
@@ -71,7 +72,8 @@ namespace Barotrauma
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (!HumanAIController.IsFriendly(character, target)) { return false; }
if (target.TurnedHostileByEvent) { return false; }
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
@@ -94,13 +96,8 @@ namespace Barotrauma
if (GetVitalityFactor(target) >= vitalityThreshold) { return false; }
}
if (target.Submarine == null || character.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null)
{
if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
}
// Don't allow going into another sub, unless it's connected and of the same team and type.
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
@@ -17,6 +17,27 @@ namespace Barotrauma
Operate
}
struct OrderInfo
{
public string ComponentIdentifier { get; set; }
public Order Order { get; private set; }
public string OrderOption { get; private set; }
public OrderInfo(Order order, string orderOption)
{
ComponentIdentifier = "currentorder";
Order = order;
OrderOption = orderOption;
}
public OrderInfo(OrderInfo orderInfo)
{
ComponentIdentifier = "previousorder";
Order = orderInfo.Order;
OrderOption = orderInfo.OrderOption;
}
}
class Order
{
public static Dictionary<string, Order> Prefabs { get; private set; }
@@ -335,7 +356,7 @@ namespace Barotrauma
return msg;
}
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub)
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, Character.TeamType? requiredTeam = null)
{
List<Item> matchingItems = new List<Item>();
if (submarine == null) { return matchingItems; }
@@ -346,12 +367,12 @@ namespace Barotrauma
Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == ItemComponentType));
if (mustBelongToPlayerSub)
{
matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player);
matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine));
matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineType.Player);
}
else
matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine));
if (requiredTeam.HasValue)
{
matchingItems.RemoveAll(it => it.Submarine != submarine);
matchingItems.RemoveAll(it => it.Submarine == null || it.Submarine.TeamID != requiredTeam.Value);
}
matchingItems.RemoveAll(it => it.NonInteractable);
if (UseController)
@@ -7,34 +7,37 @@ namespace Barotrauma
{
class PathNode
{
private WayPoint wayPoint;
private int wayPointID;
private readonly int wayPointID;
public int state;
public PathNode Parent;
private Vector2 position;
public float F,G,H;
public float F, G, H;
public List<PathNode> connections;
public List<float> distances;
public WayPoint Waypoint
{
get { return wayPoint; }
}
public Vector2 TempPosition;
public float TempDistance;
public WayPoint Waypoint { get; private set; }
public Vector2 Position
{
get { return position; }
}
public override string ToString()
{
return $"PathNode {wayPointID}";
}
public PathNode(WayPoint wayPoint)
{
this.wayPoint = wayPoint;
this.Waypoint = wayPoint;
this.position = wayPoint.SimPosition;
wayPointID = wayPoint.ID;
@@ -57,15 +60,14 @@ namespace Barotrauma
nodes.Add(wayPoint.ID, new PathNode(wayPoint));
}
foreach (KeyValuePair<int,PathNode> node in nodes)
foreach (KeyValuePair<int, PathNode> node in nodes)
{
foreach (MapEntity linked in node.Value.wayPoint.linkedTo)
foreach (MapEntity linked in node.Value.Waypoint.linkedTo)
{
PathNode connectedNode = null;
nodes.TryGetValue(linked.ID, out connectedNode);
nodes.TryGetValue(linked.ID, out PathNode connectedNode);
if (connectedNode == null) { continue; }
node.Value.connections.Add(connectedNode);
if (!node.Value.connections.Contains(connectedNode)) { node.Value.connections.Add(connectedNode); }
if (!connectedNode.connections.Contains(node.Value)) { connectedNode.connections.Add(node.Value); }
}
}
@@ -74,10 +76,10 @@ namespace Barotrauma
foreach (PathNode node in nodeList)
{
node.distances = new List<float>();
for (int i = 0; i< node.connections.Count; i++)
for (int i = 0; i < node.connections.Count; i++)
{
node.distances.Add(Vector2.Distance(node.position, node.connections[i].position));
}
}
}
return nodeList;
@@ -89,7 +91,7 @@ namespace Barotrauma
public delegate float? GetNodePenaltyHandler(PathNode node, PathNode prevNode);
public GetNodePenaltyHandler GetNodePenalty;
private List<PathNode> nodes;
private readonly List<PathNode> nodes;
public bool InsideSubmarine { get; set; }
@@ -135,8 +137,7 @@ namespace Barotrauma
{
for (int i = 0; i < wp.linkedTo.Count; i++)
{
WayPoint connected = wp.linkedTo[i] as WayPoint;
if (connected == null) { continue; }
if (!(wp.linkedTo[i] is WayPoint connected)) { continue; }
//already connected, continue
if (node.connections.Any(n => n.Waypoint == connected)) { continue; }
@@ -157,47 +158,53 @@ 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)
{
float closestDist = 0.0f;
PathNode startNode = null;
{
//sort nodes roughly according to distance
sortedNodes.Clear();
foreach (PathNode node in nodes)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
Vector2 nodePos = node.Position;
node.TempPosition = node.Position;
if (hostSub != null)
{
Vector2 diff = hostSub.SimPosition - node.Waypoint.Submarine.SimPosition;
nodePos -= diff;
node.TempPosition -= diff;
}
float xDiff = Math.Abs(start.X - node.TempPosition.X);
float yDiff = Math.Abs(start.Y - node.TempPosition.Y);
if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null) { yDiff += 10.0f; }
node.TempDistance = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
float xDiff = Math.Abs(start.X - nodePos.X);
float yDiff = Math.Abs(start.Y - nodePos.Y);
if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null)
{
yDiff += 10.0f;
}
float dist = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null && InsideSubmarine) { node.TempDistance *= 10.0f; }
//prefer nodes that are closer to the end position
dist += (Math.Abs(end.X - nodePos.X) + Math.Abs(end.Y - nodePos.Y)) / 2.0f;
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null && InsideSubmarine)
node.TempDistance += (Math.Abs(end.X - node.TempPosition.X) + Math.Abs(end.Y - node.TempPosition.Y)) / 100.0f;
int i = 0;
while (i < sortedNodes.Count && sortedNodes[i].TempDistance < node.TempDistance)
{
dist *= 10.0f;
i++;
}
if (dist < closestDist || startNode == null)
sortedNodes.Insert(i, node);
}
//find the most suitable start node, starting from the ones that are the closest
PathNode startNode = null;
foreach (PathNode node in sortedNodes)
{
if (startNode == null || node.TempDistance < startNode.TempDistance)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine)
{
var body = Submarine.PickBody(
start, nodePos, null,
start, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
//if (body.UserData is Submarine) continue;
@@ -205,8 +212,6 @@ namespace Barotrauma
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
closestDist = dist;
startNode = node;
}
}
@@ -216,36 +221,45 @@ namespace Barotrauma
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed);
#endif
return new SteeringPath(true);
}
closestDist = 0.0f;
PathNode endNode = null;
//sort nodes again, now based on distance from the end position
sortedNodes.Clear();
foreach (PathNode node in nodes)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
Vector2 nodePos = node.Position;
if (hostSub != null)
{
Vector2 diff = hostSub.SimPosition - node.Waypoint.Submarine.SimPosition;
nodePos -= diff;
}
float dist = Vector2.DistanceSquared(end, nodePos);
node.TempDistance = Vector2.DistanceSquared(end, node.TempPosition);
if (InsideSubmarine)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) { dist *= 10.0f; }
if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; }
//avoid stopping at a doorway
if (node.Waypoint.ConnectedDoor != null) { dist *= 10.0f; }
if (node.Waypoint.ConnectedDoor != null) { node.TempDistance *= 10.0f; }
//avoid stopping at a ladder
if (node.Waypoint.Ladders != null) { node.TempDistance *= 10.0f; }
}
if (dist < closestDist || endNode == null)
int i = 0;
while (i < sortedNodes.Count && sortedNodes[i].TempDistance < node.TempDistance)
{
i++;
}
sortedNodes.Insert(i, node);
}
//find the most suitable end node, starting from the ones closest to the end position
PathNode endNode = null;
foreach (PathNode node in sortedNodes)
{
if (endNode == null || node.TempDistance < endNode.TempDistance)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine)
{
var body = Submarine.PickBody(end, nodePos, null,
var body = Submarine.PickBody(end, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs );
if (body != null)
@@ -255,8 +269,6 @@ namespace Barotrauma
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
closestDist = dist;
endNode = node;
}
}
@@ -269,25 +281,25 @@ namespace Barotrauma
return new SteeringPath(true);
}
var path = FindPath(startNode, endNode, nodeFilter);
var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr);
return path;
}
public SteeringPath FindPath(WayPoint start, WayPoint end)
{
PathNode startNode=null, endNode=null;
PathNode startNode = null, endNode = null;
foreach (PathNode node in nodes)
{
if (node.Waypoint == start)
{
startNode = node;
if (endNode != null) break;
if (endNode != null) { break; }
}
if (node.Waypoint == end)
{
endNode = node;
if (startNode != null) break;
if (startNode != null) { break; }
}
}
@@ -302,13 +314,12 @@ namespace Barotrauma
return FindPath(startNode, endNode);
}
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null)
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "")
{
if (start == end)
{
var path1 = new SteeringPath();
path1.AddNode(start.Waypoint);
return path1;
}
@@ -328,8 +339,8 @@ namespace Barotrauma
float dist = float.MaxValue;
foreach (PathNode node in nodes)
{
if (filter != null && !filter(node)) { continue; }
if (node.state != 1) { continue; }
if (filter != null && !filter(node)) { continue; }
if (node.F < dist)
{
dist = node.F;
@@ -395,7 +406,7 @@ namespace Barotrauma
if (end.state == 0 || end.Parent == null)
{
#if DEBUG
DebugConsole.NewMessage("Path not found", Color.Yellow);
DebugConsole.NewMessage("Path not found. " + errorMsgStr, Color.Yellow);
#endif
return new SteeringPath(true);
}
@@ -425,15 +436,12 @@ namespace Barotrauma
}
finalPath.Add(start.Waypoint);
finalPath.Reverse();
foreach (WayPoint wayPoint in finalPath)
for (int i = finalPath.Count - 1; i >= 0; i--)
{
path.AddNode(wayPoint);
path.AddNode(finalPath[i]);
}
System.Diagnostics.Debug.Assert(finalPath.Count == path.Nodes.Count);
return path;
}
}
@@ -194,7 +194,7 @@ namespace Barotrauma
float minDist = Sonar.DefaultSonarRange * 2.0f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
{
someoneNearby = true;
@@ -82,7 +82,7 @@ namespace Barotrauma
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (Controlled == this) { return; }
if (!IsRemotePlayer)
if (!IsRemotelyControlled)
{
aiController.Update(deltaTime);
}
@@ -221,7 +221,7 @@ namespace Barotrauma
//don't flip when simply physics is enabled
if (SimplePhysicsEnabled) { return; }
if (!character.IsRemotePlayer && (character.AIController == null || character.AIController.CanFlip))
if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip))
{
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
@@ -323,6 +323,7 @@ namespace Barotrauma
levitatingCollider = true;
ColliderIndex = Crouching ? 1 : 0;
if (character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false)
{
Crouching = false;
@@ -567,7 +568,7 @@ namespace Barotrauma
//TODO: take into account that the feet aren't necessarily in CurrentHull
//full slowdown (1.5f) when water is up to the torso
surfaceY = ConvertUnits.ToSimUnits(currentHull.Surface);
float bottomPos = Math.Max(colliderPos.Y, currentHull.Rect.Y - currentHull.Rect.Height);
float bottomPos = Math.Max(colliderPos.Y, ConvertUnits.ToSimUnits(currentHull.Rect.Y - currentHull.Rect.Height));
slowdownAmount = MathHelper.Clamp((surfaceY - bottomPos) / TorsoPosition.Value, 0.0f, 1.0f) * 1.5f;
}
@@ -609,7 +610,7 @@ namespace Barotrauma
if (torso == null) { return; }
bool isNotRemote = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) isNotRemote = !character.IsRemotePlayer;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { isNotRemote = !character.IsRemotelyControlled; }
if (onGround && isNotRemote)
{
@@ -659,30 +660,36 @@ namespace Barotrauma
float y = colliderPos.Y + stepLift;
if (TorsoPosition.HasValue)
if (!torso.Disabled)
{
y += TorsoPosition.Value;
if (TorsoPosition.HasValue)
{
y += TorsoPosition.Value;
}
torso.PullJointWorldAnchorB =
MathUtils.SmoothStep(torso.SimPosition,
new Vector2(footMid + movement.X * TorsoLeanAmount, y), getUpForce);
}
torso.PullJointWorldAnchorB =
MathUtils.SmoothStep(torso.SimPosition,
new Vector2(footMid + movement.X * TorsoLeanAmount, y), getUpForce);
y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier;
if (HeadPosition.HasValue)
if (!head.Disabled)
{
y += HeadPosition.Value;
y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier;
if (HeadPosition.HasValue)
{
y += HeadPosition.Value;
}
head.PullJointWorldAnchorB =
MathUtils.SmoothStep(head.SimPosition,
new Vector2(footMid + movement.X * HeadLeanAmount, y), getUpForce * 1.2f);
}
head.PullJointWorldAnchorB =
MathUtils.SmoothStep(head.SimPosition,
new Vector2(footMid + movement.X * HeadLeanAmount, y), getUpForce * 1.2f);
if (waist != null)
if (waist != null && !waist.Disabled)
{
waist.PullJointWorldAnchorB = waist.SimPosition + movement * 0.06f;
}
}
if (TorsoAngle.HasValue)
if (TorsoAngle.HasValue && !torso.Disabled)
{
float torsoAngle = TorsoAngle.Value;
float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes");
@@ -787,8 +794,14 @@ namespace Barotrauma
if (Crouching)
{
footPos = new Vector2(
waistPos.X + Math.Sign(stepSize.X * i) * Dir * 0.1f,
colliderPos.Y - 0.1f);
Math.Sign(stepSize.X * i) * Dir * 0.4f,
colliderPos.Y);
if (Math.Sign(footPos.X) != Math.Sign(Dir))
{
//lift the foot at the back up a bit
footPos.Y += 0.15f;
}
footPos.X += torso.SimPosition.X;
}
else
{
@@ -852,7 +865,7 @@ namespace Barotrauma
{
Collider.LinearVelocity = movement;
}
else if (onGround && (!character.IsRemotePlayer || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)))
else if (onGround && (!character.IsRemotelyControlled || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)))
{
Collider.LinearVelocity = new Vector2(
movement.X,
@@ -921,7 +934,8 @@ namespace Barotrauma
rotation = MathHelper.ToDegrees(rotation);
if (rotation < 0.0f) rotation += 360;
if (!character.IsRemotePlayer && !aiming && Anim != Animation.UsingConstruction)
if (!character.IsRemotelyControlled && !aiming && Anim != Animation.UsingConstruction &&
!(character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false))
{
if (rotation > 20 && rotation < 170)
TargetDir = Direction.Left;
@@ -981,7 +995,6 @@ namespace Barotrauma
{
//pull head above water
head.body.SmoothRotate(0.0f, 5.0f);
WalkPos += 0.05f;
}
else
@@ -999,7 +1012,7 @@ namespace Barotrauma
}
bool isNotRemote = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) isNotRemote = !character.IsRemotePlayer;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { isNotRemote = !character.IsRemotelyControlled; }
if (isNotRemote)
{
@@ -1010,9 +1023,18 @@ namespace Barotrauma
legCyclePos += Math.Min(movement.LengthSquared() + Collider.AngularVelocity, 1.0f);
handCyclePos += MathHelper.ToRadians(CurrentSwimParams.HandCycleSpeed) * Math.Sign(movement.X);
float legMoveMultiplier = 1.0f;
if (movement.LengthSquared() < 0.001f)
{
//TODO: expose these?
legMoveMultiplier = 0.3f;
legCyclePos += 0.4f;
handCyclePos += 0.1f;
}
var waist = GetLimb(LimbType.Waist);
footPos = waist == null ? Vector2.Zero : waist.SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength);
Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount, 0.0f);
Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount * legMoveMultiplier, 0.0f);
transformedFootPos = Vector2.Transform(transformedFootPos, Matrix.CreateRotationZ(Collider.Rotation));
float torque = CurrentSwimParams.FootRotateStrength * character.SpeedMultiplier * (1.2f - character.GetLegPenalty());
@@ -1085,7 +1107,7 @@ namespace Barotrauma
void UpdateClimbing()
{
if (character.SelectedConstruction == null || character.SelectedConstruction.GetComponent<Ladder>() == null)
if (character.SelectedConstruction == null || character.SelectedConstruction.GetComponent<Ladder>() == null || character.IsIncapacitated)
{
Anim = Animation.None;
return;
@@ -1109,6 +1131,8 @@ namespace Barotrauma
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
if (leftHand == null || rightHand == null || head == null || torso == null) { return; }
Vector2 ladderSimPos = ConvertUnits.ToSimUnits(
character.SelectedConstruction.Rect.X + character.SelectedConstruction.Rect.Width / 2.0f,
character.SelectedConstruction.Rect.Y);
@@ -1121,10 +1145,14 @@ namespace Barotrauma
{
ladderSimPos += character.SelectedConstruction.Submarine.SimPosition;
}
else if (currentHull.Submarine != null && currentHull.Submarine != character.SelectedConstruction.Submarine)
else if (currentHull?.Submarine != null && currentHull.Submarine != character.SelectedConstruction.Submarine && character.SelectedConstruction.Submarine != null)
{
ladderSimPos += character.SelectedConstruction.Submarine.SimPosition - currentHull.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && character.SelectedConstruction.Submarine == null)
{
ladderSimPos -= currentHull.Submarine.SimPosition;
}
float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.radius - Collider.height / 2.0f;
@@ -1162,7 +1190,7 @@ namespace Barotrauma
//only move the feet if they're above the bottom of the ladders
//(if not, they'll just dangle in air, and the character holds itself up with it's arms)
if (footPos.Y > -ladderSimSize.Y)
if (footPos.Y > -ladderSimSize.Y && leftFoot != null && rightFoot != null)
{
if (slide)
{
@@ -1227,7 +1255,7 @@ namespace Barotrauma
bool isClimbing = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
isRemote = character.IsRemotePlayer;
isRemote = character.IsRemotelyControlled;
}
if (isRemote)
{
@@ -1694,7 +1722,8 @@ namespace Barotrauma
// TODO: Remove this. Provide the position in params.
Vector2 itemPos = aim ? aimPos : holdPos;
bool usingController = character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Controller>() != null;
var controller = character.SelectedConstruction?.GetComponent<Controller>();
bool usingController = controller != null && !controller.AllowAiming;
bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f;
float itemAngle;
@@ -1813,17 +1842,14 @@ namespace Barotrauma
}
}
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
if (!isClimbing)
if (!isClimbing && !character.IsIncapacitated)
{
for (int i = 0; i < 2; i++)
{
if (character.SelectedItems[i] != item) continue;
if (itemPos == Vector2.Zero) continue;
if (character.SelectedItems[i] != item || itemPos == Vector2.Zero) { continue; }
Limb hand = (i == 0) ? rightHand : leftHand;
HandIK(hand, transformedHoldPos + transformedHandlePos[i]);
}
}
@@ -47,7 +47,9 @@ namespace Barotrauma
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
protected Hull currentHull;
private bool accessRemovedCharacterErrorShown;
private Limb[] limbs;
public Limb[] Limbs
{
@@ -55,16 +57,17 @@ namespace Barotrauma
{
if (limbs == null)
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
#if DEBUG || UNSTABLE
errorMsg += '\n' + Environment.StackTrace;
#endif
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.Limbs:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace);
if (!accessRemovedCharacterErrorShown)
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
errorMsg += '\n' + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.Limbs:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace);
accessRemovedCharacterErrorShown = true;
}
return new Limb[0];
}
return limbs;
@@ -170,7 +173,7 @@ namespace Barotrauma
pos1.Y -= collider[colliderIndex].height * ColliderHeightFromFloor;
Vector2 pos2 = pos1;
pos2.Y += collider[value].height * 1.1f;
if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall))) { return; }
if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall) && !(f.Body.UserData is Submarine))) { return; }
}
Vector2 pos = collider[colliderIndex].SimPosition;
@@ -616,6 +619,7 @@ namespace Barotrauma
public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact)
{
if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; }
if (f2.UserData is Hull && character.Submarine != null) { return false; }
//using the velocity of the limb would make the impact damage more realistic,
//but would also make it harder to edit the animations because the forces/torques
@@ -690,14 +694,14 @@ namespace Barotrauma
private void ApplyImpact(Fixture f1, Fixture f2, Vector2 localNormal, Vector2 impactPos, Vector2 velocity)
{
if (character.DisableImpactDamageTimer > 0.0f) return;
if (character.DisableImpactDamageTimer > 0.0f) { return; }
Vector2 normal = localNormal;
float impact = Vector2.Dot(velocity, -normal);
if (f1.Body == Collider.FarseerBody || !Collider.Enabled)
{
bool isNotRemote = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) isNotRemote = !character.IsRemotePlayer;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { isNotRemote = !character.IsRemotelyControlled; }
if (isNotRemote)
{
@@ -930,7 +934,7 @@ namespace Barotrauma
if (setSubmarine)
{
//in -> out
if (newHull == null && currentHull.Submarine != null)
if (newHull?.Submarine == null && currentHull?.Submarine != null)
{
//don't teleport out yet if the character is going through a gap
if (Gap.FindAdjacent(currentHull.ConnectedGaps, findPos, 150.0f) != null) { return; }
@@ -1259,6 +1263,7 @@ namespace Barotrauma
return false;
}
bool isColliderValid = CheckValidity(Collider);
if (!isColliderValid) { Collider.ResetDynamics(); }
bool limbsValid = true;
foreach (Limb limb in limbs)
{
@@ -1266,6 +1271,7 @@ namespace Barotrauma
if (!CheckValidity(limb.body))
{
limbsValid = false;
limb.body.ResetDynamics();
break;
}
}
@@ -1273,11 +1279,12 @@ namespace Barotrauma
if (!isValid)
{
validityResets++;
if (validityResets > 1)
if (validityResets > 3)
{
Invalid = true;
DebugConsole.ThrowError("Invalid ragdoll physics. Ragdoll freezed to prevent crashes.");
DebugConsole.ThrowError("Invalid ragdoll physics. Ragdoll frozen to prevent crashes.");
Collider.SetTransform(Vector2.Zero, 0.0f);
Collider.ResetDynamics();
foreach (Limb limb in Limbs)
{
limb.body?.SetTransform(Collider.SimPosition, 0.0f);
@@ -1310,7 +1317,7 @@ namespace Barotrauma
}
if (errorMsg != null)
{
if (character.IsRemotePlayer)
if (character.IsRemotelyControlled)
{
errorMsg += " Ragdoll controlled remotely.";
}
@@ -1489,6 +1496,7 @@ namespace Barotrauma
case Physics.CollisionWall:
case Physics.CollisionLevel:
if (!fixture.CollidesWith.HasFlag(Physics.CollisionCharacter)) { return -1; }
if (fixture.Body.UserData is Submarine && character.Submarine != null) { return -1; }
if (fraction < standOnFloorFraction)
{
standOnFloorFraction = fraction;
@@ -57,7 +57,33 @@ namespace Barotrauma
public Hull PreviousHull = null;
public Hull CurrentHull = null;
public bool IsRemotePlayer;
/// <summary>
/// Is the character controlled remotely (either by another player, or a server-side AIController)
/// </summary>
public bool IsRemotelyControlled
{
get
{
if (GameMain.NetworkMember == null)
{
return false;
}
else if (GameMain.NetworkMember.IsClient)
{
//all characters except the client's own character are controlled by the server
return this != Controlled;
}
else
{
return IsRemotePlayer;
}
}
}
/// <summary>
/// Is the character controlled by another human player (should always be false in single player)
/// </summary>
public bool IsRemotePlayer { get; set; }
public bool IsPlayer => Controlled == this || IsRemotePlayer;
public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled;
@@ -91,10 +117,12 @@ namespace Barotrauma
set
{
teamID = value;
if (info != null) info.TeamID = value;
if (info != null) { info.TeamID = value; }
}
}
public bool TurnedHostileByEvent;
public AnimController AnimController;
private Vector2 cursorPosition;
@@ -240,7 +268,14 @@ namespace Barotrauma
var displayName = Params.DisplayName;
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = TextManager.Get($"Character.{SpeciesName}", returnNull: true);
if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride))
{
displayName = TextManager.Get($"Character.{SpeciesName}", returnNull: true);
}
else
{
displayName = TextManager.Get($"Character.{Params.SpeciesTranslationOverride}", returnNull: true);
}
}
return string.IsNullOrWhiteSpace(displayName) ? Name : displayName;
}
@@ -287,6 +322,11 @@ namespace Barotrauma
public string customInteractHUDText;
private Action<Character, Character> onCustomInteract;
public bool AllowCustomInteract
{
get { return !IsIncapacitated && Stun <= 0.0f && !Removed; }
}
private float lockHandsTimer;
public bool LockHands
{
@@ -589,30 +629,42 @@ namespace Barotrauma
set { canInventoryBeAccessed = value; }
}
public bool CanAim
{
get
{
return SelectedConstruction == null || SelectedConstruction.GetComponent<Ladder>() != null || (SelectedConstruction.GetComponent<Controller>()?.AllowAiming ?? false);
}
}
public CampaignMode.InteractionType CampaignInteractionType;
private bool accessRemovedCharacterErrorShown;
public override Vector2 SimPosition
{
get
{
if (AnimController?.Collider == null)
{
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + ".";
if (AnimController == null)
if (!accessRemovedCharacterErrorShown)
{
errorMsg += " AnimController == null";
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + ".";
if (AnimController == null)
{
errorMsg += " AnimController == null";
}
else if (AnimController.Collider == null)
{
errorMsg += " AnimController.Collider == null";
}
errorMsg += '\n' + Environment.StackTrace;
DebugConsole.NewMessage(errorMsg, Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg + "\n" + Environment.StackTrace);
accessRemovedCharacterErrorShown = true;
}
else if (AnimController.Collider == null)
{
errorMsg += " AnimController.Collider == null";
}
#if DEBUG || UNSTABLE
errorMsg += '\n' + Environment.StackTrace;
#endif
DebugConsole.NewMessage(errorMsg, Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg + "\n" + Environment.StackTrace);
return Vector2.Zero;
}
@@ -756,6 +808,10 @@ namespace Barotrauma
Info = new CharacterInfo(CharacterPrefab.HumanSpeciesName);
}
}
if (Info != null)
{
teamID = Info.TeamID;
}
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
@@ -1044,10 +1100,24 @@ namespace Barotrauma
public void GiveJobItems(WayPoint spawnPoint = null)
{
if (info == null || info.Job == null) { return; }
if (info?.Job == null) { return; }
info.Job.GiveJobItems(this, spawnPoint);
}
public void GiveIdCardTags(WayPoint spawnPoint)
{
if (info?.Job == null || spawnPoint == null) { return; }
foreach (Item item in Inventory.Items)
{
if (item?.Prefab.Identifier != "idcard") { continue; }
foreach (string s in spawnPoint.IdCardTags)
{
item.AddTag(s);
}
}
}
public float GetSkillLevel(string skillIdentifier)
{
return (Info == null || Info.Job == null) ? 0.0f : Info.Job.GetSkillLevel(skillIdentifier);
@@ -1150,27 +1220,34 @@ namespace Barotrauma
float reduction = 0;
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightFoot, excludeSevered: false), reduction);
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftFoot, excludeSevered: false), reduction);
if (!(AnimController is HumanoidAnimController))
if (AnimController is HumanoidAnimController)
{
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), reduction);
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), reduction);
}
int totalTailLimbs = 0;
int destroyedTailLimbs = 0;
foreach (var limb in AnimController.Limbs)
{
if (limb.type == LimbType.Tail)
if (AnimController.InWater)
{
totalTailLimbs++;
if (limb.IsSevered)
{
destroyedTailLimbs++;
}
// Currently only humans use hands for swimming.
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), reduction);
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), reduction);
}
}
if (destroyedTailLimbs > 0)
else
{
reduction += MathHelper.Lerp(0, AnimController.InWater ? 1f : 0.5f, (float)destroyedTailLimbs / totalTailLimbs);
int totalTailLimbs = 0;
int destroyedTailLimbs = 0;
foreach (var limb in AnimController.Limbs)
{
if (limb.type == LimbType.Tail)
{
totalTailLimbs++;
if (limb.IsSevered)
{
destroyedTailLimbs++;
}
}
}
if (destroyedTailLimbs > 0)
{
reduction += MathHelper.Lerp(0, AnimController.InWater ? 1f : 0.5f, (float)destroyedTailLimbs / totalTailLimbs);
}
}
return Math.Clamp(reduction, 0, 1f);
}
@@ -1236,8 +1313,8 @@ namespace Barotrauma
SmoothedCursorPosition = cursorPosition - smoothedCursorDiff;
}
bool playerControlled = !(this is AICharacter) || Controlled == this || IsRemotePlayer;
if (playerControlled)
bool aiControlled = this is AICharacter && Controlled != this && !IsRemotelyControlled;
if (!aiControlled)
{
Vector2 targetMovement = GetTargetMovement();
AnimController.TargetMovement = targetMovement;
@@ -1249,7 +1326,7 @@ namespace Barotrauma
((HumanoidAnimController)AnimController).Crouching = IsKeyDown(InputType.Crouch);
}
if (playerControlled &&
if (!aiControlled &&
AnimController.onGround &&
!AnimController.InWater &&
AnimController.Anim != AnimController.Animation.UsingConstruction &&
@@ -1277,7 +1354,7 @@ namespace Barotrauma
{
if (GameMain.NetworkMember.IsServer)
{
if (playerControlled)
if (!aiControlled)
{
if (dequeuedInput.HasFlag(InputNetFlags.FacingLeft))
{
@@ -1447,7 +1524,7 @@ namespace Barotrauma
}
}
if (IsRemotePlayer && keys != null)
if (IsRemotelyControlled && keys != null)
{
foreach (Key key in keys)
{
@@ -1460,15 +1537,44 @@ namespace Barotrauma
{
if (target.Removed) { return false; }
Limb seeingLimb = GetSeeingLimb();
return target.AnimController.Limbs.Any(l => CanSeeTarget(l, seeingLimb));
if (CanSeeTarget(target, seeingLimb)) { return true; }
if (!target.AnimController.SimplePhysicsEnabled)
{
//find the limbs that are furthest from the target's position (from the viewer's point of view)
Limb leftExtremity = null, rightExtremity = null;
float leftMostDot = 0.0f, rightMostDot = 0.0f;
Vector2 dir = target.WorldPosition - WorldPosition;
Vector2 leftDir = new Vector2(dir.Y, -dir.X);
Vector2 rightDir = new Vector2(-dir.Y, dir.X);
foreach (Limb limb in target.AnimController.Limbs)
{
if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; }
Vector2 limbDir = limb.WorldPosition - WorldPosition;
float leftDot = Vector2.Dot(limbDir, leftDir);
if (leftDot > leftMostDot)
{
leftMostDot = leftDot;
leftExtremity = limb;
continue;
}
float rightDot = Vector2.Dot(limbDir, rightDir);
if (rightDot > rightMostDot)
{
rightMostDot = rightDot;
rightExtremity = limb;
continue;
}
}
if (leftExtremity != null && CanSeeTarget(leftExtremity, seeingLimb)) { return true; }
if (rightExtremity != null && CanSeeTarget(rightExtremity, seeingLimb)) { return true; }
}
return false;
}
private Limb GetSeeingLimb()
{
Limb selfLimb = AnimController.GetLimb(LimbType.Head);
if (selfLimb == null) { selfLimb = AnimController.GetLimb(LimbType.Torso); }
if (selfLimb == null) { selfLimb = AnimController.MainLimb; }
return selfLimb;
return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb;
}
public bool CanSeeTarget(ISpatialEntity target, Limb seeingLimb = null)
@@ -1531,12 +1637,12 @@ namespace Barotrauma
if (target.Submarine == null)
{
closestBody = Submarine.CheckVisibility(sourceWorldPos, sourceWorldPos + diff);
if (closestBody == null) return true;
if (closestBody == null) { return true; }
}
else
{
closestBody = Submarine.CheckVisibility(target.WorldPosition, target.WorldPosition - diff);
if (closestBody == null) return true;
if (closestBody == null) { return true; }
}
Structure wall = closestBody.UserData as Structure;
Item item = closestBody.UserData as Item;
@@ -1544,6 +1650,11 @@ namespace Barotrauma
return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen || door.IsBroken);
}
/// <summary>
/// A simple check if the character Dir is towards the target or not. Uses the world coordinates.
/// </summary>
public bool IsFacing(Vector2 targetWorldPos) => AnimController.Dir > 0 && targetWorldPos.X > WorldPosition.X || AnimController.Dir < 0 && targetWorldPos.X < WorldPosition.X;
public bool HasItem(Item item, bool requireEquipped = false) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this);
public bool HasEquippedItem(Item item)
@@ -1551,7 +1662,7 @@ namespace Barotrauma
if (Inventory == null) { return false; }
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.Items[i] == item && Inventory.SlotTypes[i] != InvSlotType.Any) return true;
if (Inventory.Items[i] == item && Inventory.SlotTypes[i] != InvSlotType.Any) { return true; }
}
return false;
@@ -1559,12 +1670,12 @@ namespace Barotrauma
public bool HasEquippedItem(string itemIdentifier, bool allowBroken = true)
{
if (Inventory == null) return false;
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.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; }
}
return false;
@@ -1597,7 +1708,7 @@ namespace Barotrauma
public bool TrySelectItem(Item item, int index)
{
if (selectedItems[index] != null) return false;
if (selectedItems[index] != null) { return false; }
selectedItems[index] = item;
return true;
@@ -1698,7 +1809,7 @@ namespace Barotrauma
public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false)
{
if (c == this || Removed || !c.Enabled || !c.CanBeSelected) { return false; }
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && c.onCustomInteract == null) { return false; }
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; }
if (!skipDistanceCheck)
{
@@ -1730,10 +1841,10 @@ namespace Barotrauma
}
Wire wire = item.GetComponent<Wire>();
if (wire != null)
if (wire != null && item.GetComponent<ConnectionPanel>() == null)
{
//locked wires are never interactable
if (wire.Locked) return false;
if (wire.Locked) { 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
@@ -2003,10 +2114,21 @@ namespace Barotrauma
#endif
}
}
else if (FocusedCharacter != null && IsKeyHit(InputType.Select) && FocusedCharacter.onCustomInteract != null)
else if (FocusedCharacter != null && IsKeyHit(InputType.Use) && FocusedCharacter.onCustomInteract != null && FocusedCharacter.AllowCustomInteract)
{
FocusedCharacter.onCustomInteract(FocusedCharacter, this);
}
else if (IsKeyHit(InputType.Deselect) && SelectedConstruction != null && SelectedConstruction.GetComponent<Ladder>() == null)
{
SelectedConstruction = null;
#if CLIENT
CharacterHealth.OpenHealthWindow = null;
#endif
}
else if (IsKeyHit(InputType.Health) && SelectedConstruction != null && SelectedConstruction.GetComponent<Ladder>() == null)
{
SelectedConstruction = null;
}
else if (focusedItem != null)
{
#if CLIENT
@@ -2023,14 +2145,7 @@ namespace Barotrauma
}
}
#endif
}
else if (IsKeyHit(InputType.Deselect) && SelectedConstruction != null && SelectedConstruction.GetComponent<Ladder>() == null)
{
SelectedConstruction = null;
#if CLIENT
CharacterHealth.OpenHealthWindow = null;
#endif
}
}
}
public static void UpdateAnimAll(float deltaTime)
@@ -2377,18 +2492,18 @@ namespace Barotrauma
}
private float despawnTimer;
private void UpdateDespawn(float deltaTime)
private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false)
{
if (!EnableDespawn) { return; }
//clients don't despawn characters unless the server says so
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (!IsDead) { return; }
if (!IsDead || (CauseOfDeath?.Type == CauseOfDeathType.Disconnected && GameMain.GameSession?.Campaign != null)) { return; }
int subCorpseCount = 0;
if (Submarine != null)
if (Submarine != null && !ignoreThresholds)
{
subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine);
if (subCorpseCount < GameMain.Config.CorpsesPerSubDespawnThreshold) { return; }
@@ -2427,12 +2542,14 @@ namespace Barotrauma
}
else
{
Spawner.AddToSpawnQueue(containerPrefab, WorldPosition, onSpawned: onItemContainerSpawned);
Spawner?.AddToSpawnQueue(containerPrefab, WorldPosition, onSpawned: onItemContainerSpawned);
}
void onItemContainerSpawned(Item item)
{
if (Inventory?.Items == null) { return; }
item.UpdateTransform();
item.AddTag("name:" + Name);
if (info?.Job != null) { item.AddTag("job:" + info.Job.Name); }
@@ -2457,6 +2574,8 @@ namespace Barotrauma
public void DespawnNow()
{
despawnTimer = GameMain.Config.CorpseDespawnDelay;
UpdateDespawn(1.0f, ignoreThresholds: true);
Spawner.Update();
}
public static void RemoveByPrefab(CharacterPrefab prefab)
@@ -2706,13 +2825,13 @@ namespace Barotrauma
GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack);
}
#endif
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage);
// Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head)
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: AIController == null || AIController is HumanAIController);
return attackResult;
}
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage)
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
#if DEBUG
@@ -2722,8 +2841,12 @@ namespace Barotrauma
return;
}
#endif
if (!IsDead && !targetLimb.CanBeSeveredAlive) { return; }
if (damage < targetLimb.Params.MinSeveranceDamage) { return; }
if (!IsDead)
{
if (!allowBeheading && targetLimb.type == LimbType.Head) { return; }
if (!targetLimb.CanBeSeveredAlive) { return; }
}
bool wasSevered = false;
float random = Rand.Value();
foreach (LimbJoint joint in AnimController.LimbJoints)
@@ -3323,7 +3446,7 @@ namespace Barotrauma
public bool IsEngineer => HasJob("engineer");
public bool IsMechanic => HasJob("mechanic");
public bool IsMedic => HasJob("medicaldoctor");
public bool IsOfficer => HasJob("securityofficer");
public bool IsSecurity => HasJob("securityofficer");
public bool IsAsssitant => HasJob("assistant");
public bool IsWatchman => HasJob("watchman");
@@ -147,6 +147,9 @@ namespace Barotrauma
}
}
public XElement InventoryData;
public XElement HealthData;
private static ushort idCounter;
public string Name;
@@ -287,21 +290,21 @@ namespace Barotrauma
public bool StartItemsGiven;
public bool IsNewHire;
public CauseOfDeath CauseOfDeath;
public Character.TeamType TeamID;
private NPCPersonalityTrait personalityTrait;
public Order CurrentOrder { get; set;}
public Order CurrentOrder { get; set; }
public string CurrentOrderOption { get; set; }
//unique ID given to character infos in MP
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
public ushort ID;
public XElement InventoryData;
public List<string> SpriteTags
{
get;
@@ -564,6 +567,22 @@ namespace Barotrauma
}
}
public int GetIdentifier()
{
int id = ToolBox.StringToInt(Name);
id ^= HeadSpriteId;
id ^= (int)Race << 6;
id ^= HairIndex << 12;
id ^= BeardIndex << 18;
id ^= MoustacheIndex << 24;
id ^= FaceAttachmentIndex << 30;
if (Job != null)
{
id ^= ToolBox.StringToInt(Job.Prefab.Identifier);
}
return id;
}
public IEnumerable<XElement> FilterByTypeAndHeadID(IEnumerable<XElement> elements, WearableType targetType)
{
if (elements == null) { return elements; }
@@ -813,20 +832,17 @@ namespace Barotrauma
partial void LoadAttachmentSprites(bool omitJob);
// TODO: change the formula so that it's not linear and so that it takes into account the usefulness of the skill
// -> give a weight to each skill, because some are much more valuable than others?
private int CalculateSalary()
{
if (Name == null || Job == null) return 0;
int salary = Math.Abs(Name.GetHashCode()) % 100;
if (Name == null || Job == null) { return 0; }
int salary = 0;
foreach (Skill skill in Job.Skills)
{
salary += (int)skill.Level * 50;
salary += (int)(skill.Level * skill.Prefab.PriceMultiplier);
}
return salary;
return (int)(salary * Job.Prefab.PriceMultiplier);
}
public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 worldPos)
@@ -871,7 +887,7 @@ namespace Barotrauma
partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos);
public virtual XElement Save(XElement parentElement)
public XElement Save(XElement parentElement)
{
XElement charElement = new XElement("Character");
@@ -971,7 +987,12 @@ namespace Barotrauma
}
}
}
public void ApplyHealthData(Character character, XElement healthData)
{
if (healthData != null) { character?.CharacterHealth.Load(healthData); }
}
public void ReloadHeadAttachments()
{
ResetLoadedAttachments();
@@ -7,7 +7,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
class CorpsePrefab : IPrefab, IDisposable
class CorpsePrefab : HumanPrefab, IPrefab, IDisposable
{
public static readonly PrefabCollection<CorpsePrefab> Prefabs = new PrefabCollection<CorpsePrefab>();
@@ -37,36 +37,13 @@ namespace Barotrauma
}
}
[Serialize("notfound", false)]
public string Identifier { get; private set; }
[Serialize("any", false)]
public string Job { get; private set; }
[Serialize(1f, false)]
public float Commonness { get; private set; }
[Serialize(Level.PositionType.Wreck, false)]
public Level.PositionType SpawnPosition { get; private set; }
public string OriginalName { get { return Identifier; } }
public ContentPackage ContentPackage { get; private set; }
public string FilePath { get; private set; }
public XElement Element { get; private set; }
public readonly Dictionary<XElement, float> ItemSets = new Dictionary<XElement, float>();
public CorpsePrefab(XElement element, string filePath, bool allowOverriding)
public CorpsePrefab(XElement element, string filePath, bool allowOverriding) : base(element, filePath)
{
FilePath = filePath;
SerializableProperty.DeserializeProperties(this, element);
Identifier = Identifier.ToLowerInvariant();
Job = Job.ToLowerInvariant();
Element = element;
element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1)));
Prefabs.Add(this, allowOverriding);
}
@@ -145,7 +122,7 @@ namespace Barotrauma
}
break;
default:
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}");
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}");
break;
}
}
@@ -153,76 +130,6 @@ namespace Barotrauma
public static void RemoveByFile(string filePath)
{
Prefabs.RemoveByFile(filePath);
}
public void GiveItems(Character character, Submarine submarine)
{
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), Rand.RandSync.Unsynced);
foreach (XElement itemElement in spawnItems.GetChildElements("item"))
{
InitializeItems(character, itemElement, submarine);
}
}
private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null)
{
ItemPrefab itemPrefab;
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Tried to spawn \"" + Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
return;
}
Item item = new Item(itemPrefab, character.Position, null);
#if SERVER
if (GameMain.Server != null && Entity.Spawner != null)
{
if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item))
{
string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item);
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
Entity.Spawner.CreateNetworkEvent(item, false);
}
#endif
if (itemElement.GetAttributeBool("equip", false))
{
List<InvSlotType> allowedSlots = new List<InvSlotType>(item.AllowedSlots);
allowedSlots.Remove(InvSlotType.Any);
character.Inventory.TryPutItem(item, null, allowedSlots);
}
else
{
character.Inventory.TryPutItem(item, null, item.AllowedSlots);
}
if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck")
{
item.AddTag("name:" + character.Name);
item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine));
var job = character.Info?.Job;
if (job != null)
{
item.AddTag("job:" + job.Name);
}
}
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = character.TeamID;
}
if (parentItem != null)
{
parentItem.Combine(item, user: null);
}
foreach (XElement childItemElement in itemElement.Elements())
{
InitializeItems(character, childItemElement, submarine, item);
}
}
}
}
}
@@ -5,6 +5,7 @@ using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Extensions;
using System.Globalization;
namespace Barotrauma
{
@@ -127,7 +128,7 @@ namespace Barotrauma
public bool IsUnconscious
{
get { return Vitality <= 0.0f; }
get { return Vitality <= 0.0f || Character.IsDead; }
}
public float PressureKillDelay { get; private set; } = 5.0f;
@@ -146,6 +147,11 @@ namespace Barotrauma
}
return maxVitality;
}
set
{
maxVitality = Math.Max(0, value);
}
}
public float MinVitality
@@ -937,7 +943,83 @@ namespace Barotrauma
partial void RemoveProjSpecific();
/// <summary>
/// Automatically filters out buffs.
/// </summary>
public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions, bool excludeBuffs = true) =>
afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength);
public void Save(XElement healthElement)
{
foreach (Affliction affliction in afflictions)
{
if (affliction.Strength <= 0.0f) { continue; }
healthElement.Add(new XElement("Affliction",
new XAttribute("identifier", affliction.Identifier),
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
}
for (int i = 0; i < limbHealths.Count; i++)
{
var limbHealthElement = new XElement("LimbHealth", new XAttribute("i", i));
healthElement.Add(limbHealthElement);
foreach (Affliction affliction in limbHealths[i].Afflictions)
{
if (affliction.Strength <= 0.0f) { continue; }
limbHealthElement.Add(new XElement("Affliction",
new XAttribute("identifier", affliction.Identifier),
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
}
}
}
public void Load(XElement element)
{
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "affliction":
LoadAffliction(subElement);
break;
case "limbhealth":
int limbHealthIndex = subElement.GetAttributeInt("i", -1);
if (limbHealthIndex < 0 || limbHealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError($"Error while loading character health: limb index \"{limbHealthIndex}\" out of range.");
continue;
}
foreach (XElement afflictionElement in subElement.Elements())
{
LoadAffliction(afflictionElement, limbHealths[limbHealthIndex]);
}
break;
}
}
void LoadAffliction(XElement afflictionElement, LimbHealth limbHealth = null)
{
string id = afflictionElement.GetAttributeString("identifier", "");
var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == id);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found.");
return;
}
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
if (irremovableAffliction != null)
{
irremovableAffliction.Strength = strength;
}
else if (limbHealth != null)
{
limbHealth.Afflictions.Add(afflictionPrefab.Instantiate(strength));
}
else
{
afflictions.Add(afflictionPrefab.Instantiate(strength));
}
}
}
}
}
@@ -0,0 +1,178 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class HumanPrefab
{
[Serialize("notfound", false)]
public string Identifier { get; protected set; }
[Serialize("any", false)]
public string Job { get; protected set; }
[Serialize(1f, false)]
public float Commonness { get; protected set; }
[Serialize(1f, false)]
public float HealthMultiplier { get; protected set; }
private readonly HashSet<string> moduleFlags = new HashSet<string>();
[Serialize("", true, "What outpost module tags does the NPC prefer to spawn in.")]
public string ModuleFlags
{
get => string.Join(",", moduleFlags);
set
{
moduleFlags.Clear();
if (!string.IsNullOrWhiteSpace(value))
{
string[] splitFlags = value.Split(',');
foreach (var f in splitFlags)
{
moduleFlags.Add(f);
}
}
}
}
private readonly HashSet<string> spawnPointTags = new HashSet<string>();
[Serialize("", true, "Tag(s) of the spawnpoints the NPC prefers to spawn at.")]
public string SpawnPointTags
{
get => string.Join(",", spawnPointTags);
set
{
spawnPointTags.Clear();
if (!string.IsNullOrWhiteSpace(value))
{
string[] splitTags = value.Split(',');
foreach (var tag in splitTags)
{
spawnPointTags.Add(tag.ToLowerInvariant());
}
}
}
}
[Serialize("None", false)]
public CampaignMode.InteractionType CampaignInteractionType { get; protected set; }
[Serialize("Passive", false)]
public AIObjectiveIdle.BehaviorType BehaviorType { get; protected set; }
public List<string> PreferredOutpostModuleTypes { get; protected set; }
public string OriginalName { get { return Identifier; } }
public string FilePath { get; protected set; }
public XElement Element { get; protected set; }
public readonly Dictionary<XElement, float> ItemSets = new Dictionary<XElement, float>();
public HumanPrefab(XElement element, string filePath)
{
FilePath = filePath;
SerializableProperty.DeserializeProperties(this, element);
Identifier = Identifier.ToLowerInvariant();
Job = Job.ToLowerInvariant();
Element = element;
element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1)));
PreferredOutpostModuleTypes = element.GetAttributeStringArray("preferredoutpostmoduletypes", new string[0], convertToLowerInvariant: true).ToList();
}
public IEnumerable<string> GetModuleFlags()
{
return moduleFlags;
}
public IEnumerable<string> GetSpawnPointTags()
{
return spawnPointTags;
}
public JobPrefab GetJobPrefab(Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync);
}
public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), randSync);
foreach (XElement itemElement in spawnItems.GetChildElements("item"))
{
InitializeItems(character, itemElement, submarine);
}
}
private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null)
{
ItemPrefab itemPrefab;
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Tried to spawn \"" + Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
return;
}
Item item = new Item(itemPrefab, character.Position, null);
#if SERVER
if (GameMain.Server != null && Entity.Spawner != null)
{
if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item))
{
string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item);
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
Entity.Spawner.CreateNetworkEvent(item, false);
}
#endif
if (itemElement.GetAttributeBool("equip", false))
{
List<InvSlotType> allowedSlots = new List<InvSlotType>(item.AllowedSlots);
allowedSlots.Remove(InvSlotType.Any);
character.Inventory.TryPutItem(item, null, allowedSlots);
}
else
{
character.Inventory.TryPutItem(item, null, item.AllowedSlots);
}
if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck")
{
item.AddTag("name:" + character.Name);
item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine));
var job = character.Info?.Job;
if (job != null)
{
item.AddTag("job:" + job.Name);
}
}
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = character.TeamID;
}
if (parentItem != null)
{
parentItem.Combine(item, user: null);
}
foreach (XElement childItemElement in itemElement.Elements())
{
InitializeItems(character, childItemElement, submarine, item);
}
}
}
}
@@ -33,6 +33,8 @@ namespace Barotrauma
public int Variant;
public Skill PrimarySkill { get; }
public Job(JobPrefab jobPrefab, int variant = 0)
{
prefab = jobPrefab;
@@ -41,14 +43,16 @@ namespace Barotrauma
skills = new Dictionary<string, Skill>();
foreach (SkillPrefab skillPrefab in prefab.Skills)
{
skills.Add(skillPrefab.Identifier, new Skill(skillPrefab));
var skill = new Skill(skillPrefab);
skills.Add(skillPrefab.Identifier, skill);
if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; }
}
}
public Job(XElement element)
{
string identifier = element.GetAttributeString("identifier", "").ToLowerInvariant();
JobPrefab p = null;
JobPrefab p;
if (!JobPrefab.Prefabs.ContainsKey(identifier))
{
DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.");
@@ -65,9 +69,9 @@ namespace Barotrauma
if (!subElement.Name.ToString().Equals("skill", System.StringComparison.OrdinalIgnoreCase)) { continue; }
string skillIdentifier = subElement.GetAttributeString("identifier", "");
if (string.IsNullOrEmpty(skillIdentifier)) { continue; }
skills.Add(
skillIdentifier,
new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0)));
var skill = new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0));
skills.Add(skillIdentifier, skill);
if (skillIdentifier == prefab.PrimarySkill?.Identifier) { PrimarySkill = skill; }
}
}
@@ -146,6 +146,13 @@ namespace Barotrauma
private set;
}
[Serialize(1.0f, false)]
public float PriceMultiplier
{
get;
private set;
}
// TODO: not used
[Serialize(10.0f, false)]
public float Commonness
@@ -164,6 +171,9 @@ namespace Barotrauma
public Sprite Icon;
public Sprite IconSmall;
public SkillPrefab PrimarySkill => Skills?.FirstOrDefault(s => s.IsPrimarySkill);
public string FilePath { get; private set; }
public XElement Element { get; private set; }
@@ -1,24 +1,12 @@
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
class Skill
{
private SkillPrefab prefab;
private float level;
static string[] levelNames = new string[] {
"Untrained", "Incompetent", "Novice",
"Adequate", "Competent", "Proficient",
"Professional", "Master", "Legendary" };
string identifier;
public string Identifier
{
get { return identifier; }
}
public string Identifier { get; }
public float Level
{
@@ -26,29 +14,58 @@ namespace Barotrauma
set { level = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
private Sprite icon;
public Sprite Icon
{
get
{
if (icon == null)
{
icon = GetIcon();
}
return icon;
}
}
internal SkillPrefab Prefab { get; private set; }
public Skill(SkillPrefab prefab)
{
this.prefab = prefab;
this.identifier = prefab.Identifier;
this.level = Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y, Rand.RandSync.Server);
this.Prefab = prefab;
Identifier = prefab.Identifier;
level = Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y, Rand.RandSync.Server);
icon = GetIcon();
}
public Skill(string identifier, float level)
{
this.identifier = identifier;
Identifier = identifier;
this.level = level;
icon = GetIcon();
}
/// <summary>
/// returns the "name" of some skill level (0-10 -> untrained, etc)
/// </summary>
public static string GetLevelName(float level)
private Sprite GetIcon()
{
level = MathHelper.Clamp(level, 0.0f, 100.0f);
int scaledLevel = (int)Math.Floor((level / 100.0f) * levelNames.Length);
return levelNames[Math.Min(scaledLevel, levelNames.Length - 1)];
string jobId = null;
switch (Identifier.ToLowerInvariant())
{
case "electrical":
jobId = "engineer";
break;
case "helm":
jobId = "captain";
break;
case "mechanical":
jobId = "mechanic";
break;
case "medical":
jobId = "medicaldoctor";
break;
case "weapons":
jobId = "securityofficer";
break;
}
return jobId != null && JobPrefab.Prefabs.ContainsKey(jobId) ? JobPrefab.Prefabs[jobId].IconSmall : null;
}
}
}
@@ -9,10 +9,17 @@ namespace Barotrauma
public Vector2 LevelRange { get; private set; }
/// <summary>
/// How much this skill affects characters' hiring cost
/// </summary>
public readonly float PriceMultiplier;
public bool IsPrimarySkill { get; }
public SkillPrefab(XElement element)
{
Identifier = element.GetAttributeString("identifier", "");
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 25.0f);
var levelString = element.GetAttributeString("level", "");
if (levelString.Contains(","))
{
@@ -23,6 +30,8 @@ namespace Barotrauma
float skillLevel = float.Parse(levelString, System.Globalization.CultureInfo.InvariantCulture);
LevelRange = new Vector2(skillLevel, skillLevel);
}
IsPrimarySkill = element.GetAttributeBool("primary", false);
}
}
}
@@ -19,6 +19,9 @@ namespace Barotrauma
[Serialize("", true), Editable]
public string SpeciesName { get; private set; }
[Serialize("", true, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable]
public string SpeciesTranslationOverride { get; private set; }
[Serialize("", true, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
public string DisplayName { get; private set; }
@@ -18,6 +18,10 @@ namespace Barotrauma
Character,
Structure,
Outpost,
OutpostModule,
OutpostConfig,
NPCSets,
Factions,
Text,
Executable,
ServerExecutable,
@@ -42,7 +46,8 @@ namespace Barotrauma
SkillSettings,
Wreck,
Corpses,
WreckAIConfig
WreckAIConfig,
UpgradeModules
}
public class ContentPackage
@@ -60,17 +65,22 @@ namespace Barotrauma
ContentType.Character,
ContentType.Structure,
ContentType.LocationTypes,
ContentType.NPCSets,
ContentType.Factions,
ContentType.MapGenerationParameters,
ContentType.LevelGenerationParameters,
ContentType.Missions,
ContentType.LevelObjectPrefabs,
ContentType.RuinConfig,
ContentType.Outpost,
ContentType.OutpostModule,
ContentType.OutpostConfig,
ContentType.Wreck,
ContentType.WreckAIConfig,
ContentType.Afflictions,
ContentType.Orders,
ContentType.Corpses
ContentType.Corpses,
ContentType.UpgradeModules
};
//at least one file of each these types is required in core content packages
@@ -80,7 +90,10 @@ namespace Barotrauma
ContentType.Item,
ContentType.Character,
ContentType.Structure,
ContentType.Outpost,
//TODO: there needs to be either outpost files or outpost generation parameters, both aren't required
//ContentType.Outpost,
//ContentType.OutpostGenerationParams,
ContentType.Factions,
ContentType.Wreck,
ContentType.WreckAIConfig,
ContentType.Text,
@@ -96,7 +109,8 @@ namespace Barotrauma
ContentType.UIStyle,
ContentType.EventManagerSettings,
ContentType.Orders,
ContentType.Corpses
ContentType.Corpses,
ContentType.UpgradeModules
};
public static IEnumerable<ContentType> CorePackageRequiredFiles
@@ -205,6 +219,22 @@ namespace Barotrauma
Files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type, this));
}
if (Files.Count == 0)
{
//no files defined, find a submarine in here
//because somehow people have managed to upload
//mods without contentfile definitions
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));
}
else
{
errorMsgs.Add("Error in content package \"" + Name + "\" - no content files defined.");
}
}
bool compatible = IsCompatible();
//If we know that the package is not compatible, don't display error messages.
if (compatible)
@@ -292,6 +322,7 @@ namespace Barotrauma
case ContentType.ServerExecutable:
case ContentType.None:
case ContentType.Outpost:
case ContentType.OutpostModule:
case ContentType.Submarine:
case ContentType.Wreck:
break;
@@ -396,7 +427,6 @@ namespace Barotrauma
new XAttribute("path", Path.CleanUpPathCrossPlatform(correctFilenameCase: false)),
new XAttribute("corepackage", CorePackage)));
doc.Root.Add(new XAttribute("gameversion", GameVersion.ToString()));
if (!string.IsNullOrEmpty(SteamWorkshopUrl))
@@ -428,7 +458,7 @@ namespace Barotrauma
reselectPackage = true;
if (p.CorePackage)
{
GameMain.Config.SelectCorePackage(List.Find(cpp => cpp.CorePackage && !packagesToDeselect.Contains(cpp)));
GameMain.Config.AutoSelectCorePackage(packagesToDeselect);
}
else
{
@@ -525,7 +555,7 @@ namespace Barotrauma
{
using (MD5 tempMd5 = MD5.Create())
{
filePaths = filePaths.OrderBy(f => ToolBox.StringToUInt32Hash(f.CleanUpPathCrossPlatform(true), tempMd5)).ToList();
filePaths = filePaths.OrderBy(f => ToolBox.StringToUInt32Hash(f.CleanUpPathCrossPlatform(true).ToLowerInvariant(), tempMd5)).ToList();
}
}
@@ -593,6 +623,10 @@ namespace Barotrauma
{
return contentPackages.SelectMany(f => f.Files).Where(f => f.Type == type);
}
public static IEnumerable<ContentFile> GetFilesOfType(IEnumerable<ContentPackage> contentPackages, params ContentType[] types)
{
return contentPackages.SelectMany(f => f.Files).Where(f => types.Contains(f.Type));
}
public IEnumerable<string> GetFilesOfType(ContentType type)
{
@@ -19,14 +19,16 @@ namespace Barotrauma
public string Text;
public Color Color;
public bool IsCommand;
public bool IsError;
public readonly string Time;
public ColoredText(string text, Color color, bool isCommand)
public ColoredText(string text, Color color, bool isCommand, bool isError)
{
this.Text = text;
this.Color = color;
this.IsCommand = isCommand;
this.IsError = isError;
Time = DateTime.Now.ToString();
}
@@ -204,8 +206,8 @@ namespace Barotrauma
return new string[][]
{
characterFiles.ToArray(),
new string[] { "near", "inside", "outside", "cursor" }
characterFiles.ToArray(),
new string[] { "near", "inside", "outside", "cursor" }
};
}, isCheat: true));
@@ -694,6 +696,87 @@ namespace Barotrauma
NewMessage(GameMain.GameSession.EventManager.Enabled ? "Event manager on" : "Event manager off", Color.White);
}
}, isCheat: true));
commands.Add(new Command("triggerevent", "triggerevent [identifier]: Created a new event.", (string[] args) =>
{
List<EventPrefab> eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => !string.IsNullOrWhiteSpace(prefab.Identifier)).ToList();
if (GameMain.GameSession?.EventManager != null && args.Length > 0)
{
EventPrefab newEvent = eventPrefabs.Find(prefab => string.Equals(prefab.Identifier, args[0], StringComparison.InvariantCultureIgnoreCase));
if (newEvent != null)
{
var @event = newEvent.CreateInstance();
GameMain.GameSession.EventManager.ActiveEvents.Add(@event);
@event.Init(true);
NewMessage($"Initialized event {newEvent.Identifier}", Color.Aqua);
return;
}
NewMessage($"Failed to trigger event because {args[0]} is not a valid event identifier.", Color.Red);
return;
}
NewMessage("Failed to trigger event", Color.Red);
}, isCheat: true, getValidArgs: () =>
{
List<EventPrefab> eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => !string.IsNullOrWhiteSpace(prefab.Identifier)).ToList();
return new[]
{
eventPrefabs.Select(prefab => prefab.Identifier).Distinct().ToArray()
};
}));
commands.Add(new Command("setskill", "setskill [all/identifier] [max/level] [character]: Set your skill level.", (string[] args) =>
{
if (args.Length < 2)
{
NewMessage($"Missing arguments. Expected at least 2 but got {args.Length} (skill, level, name)", Color.Red);
return;
}
string skillIdentifier = args[0];
string levelString = args[1];
Character character = args.Length >= 3 ? FindMatchingCharacter(args.Skip(2).ToArray(), false) : Character.Controlled;
if (character?.Info?.Job == null)
{
NewMessage("Character is not valid.", Color.Red);
return;
}
bool isMax = levelString.Equals("max", StringComparison.OrdinalIgnoreCase);
if (float.TryParse(levelString, NumberStyles.Number, CultureInfo.InvariantCulture, out float level) || isMax)
{
if (isMax) { level = 100; }
if (skillIdentifier.Equals("all", StringComparison.OrdinalIgnoreCase))
{
foreach (Skill skill in character.Info.Job.Skills)
{
character.Info.SetSkillLevel(skill.Identifier, level, character.WorldPosition);
}
NewMessage($"Set all {character.Name}'s skills to {level}", Color.Green);
}
else
{
character.Info.SetSkillLevel(skillIdentifier, level, character.WorldPosition);
NewMessage($"Set {character.Name}'s {skillIdentifier} level to {level}", Color.Green);
}
}
else
{
NewMessage($"{levelString} is not a valid level. Expected number or \"max\".", Color.Red);
}
}, isCheat: true, getValidArgs: () =>
{
return new[]
{
Character.Controlled?.Info?.Job?.Skills?.Select(skill => skill.Identifier).ToArray() ?? new string[0],
new[]{ "max" },
Character.CharacterList.Select(c => c.Name).Distinct().ToArray(),
};
}));
commands.Add(new Command("water|editwater", "water/editwater: Toggle water editing. Allows adding water into rooms by holding the left mouse button and removing it by holding the right mouse button.", (string[] args) =>
{
@@ -724,6 +807,11 @@ namespace Barotrauma
commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) =>
{
if (Submarine.MainSub == null || Level.Loaded == null) return;
if (Level.Loaded.Type == LevelData.LevelType.Outpost)
{
NewMessage("The teleportsub command is unavailable in outpost levels!", Color.Red);
return;
}
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
{
@@ -735,11 +823,21 @@ namespace Barotrauma
}
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
Vector2 pos = Level.Loaded.StartPosition;
if (Level.Loaded.StartOutpost != null)
{
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
}
Submarine.MainSub.SetPosition(pos);
}
else
{
Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
Vector2 pos = Level.Loaded.EndPosition;
if (Level.Loaded.EndOutpost != null)
{
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
}
Submarine.MainSub.SetPosition(pos);
}
},
() =>
@@ -802,10 +900,8 @@ namespace Barotrauma
while (true)
{
var gamesession = new GameSession(
SubmarineInfo.SavedSubmarines.GetRandom(s => !s.HasTag(SubmarineTag.HideInMenus)),
"Data/Saves/test.xml",
GameModePreset.List.Find(gm => gm.Identifier == "devsandbox"),
missionPrefab: null);
SubmarineInfo.SavedSubmarines.GetRandom(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)),
GameModePreset.DevSandbox);
string seed = ToolBox.RandomSeed(16);
gamesession.StartRound(seed);
@@ -856,11 +952,31 @@ namespace Barotrauma
}
#endif
commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) =>
commands.Add(new Command("setlocationreputation", "setlocationreputation [value]: Set the reputation in the current location to the specified value.", (string[] args) =>
{
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
if (args.Length == 0) { return; }
if (float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation))
{
campaign.Map.CurrentLocation.Reputation.Value = reputation;
}
else
{
ThrowError("Could not set location reputation ({args[0]} is not a valid reputation value).");
}
}
else
{
ThrowError("Could not set location reputation (no active campaign).");
}
}, null, true));
commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) =>
{
foreach (Item it in Item.ItemList)
{
it.Condition = it.Prefab.Health;
it.Condition = it.MaxCondition;
}
}, null, true));
@@ -883,21 +999,109 @@ namespace Barotrauma
}
}
}, null, true));
commands.Add(new Command("upgradeitem", "upgradeitem [upgrade] [level] [items]: Adds an upgrade to the current targeted item.", args =>
{
if (args.Length > 0)
{
int level;
if (args.Length > 1)
{
if (int.TryParse(args[1], out int result))
{
level = result;
}
else
{
ThrowError($"\"{args[1]}\" is not a valid level.");
return;
}
}
else
{
ThrowError("Parameter \"level\" is required.");
return;
}
commands.Add(new Command("power", "power [temperature]: Immediately sets the temperature of the nuclear reactor to the specified value.", (string[] args) =>
var upgradePrefab = UpgradePrefab.Find(args[0]);
if (upgradePrefab == null)
{
ThrowError($"Unknown upgrade: {args[0]}.");
return;
}
List<MapEntity> targetItems = new List<MapEntity>();
if (upgradePrefab.IsWallUpgrade)
{
targetItems.AddRange(Submarine.MainSub.GetWalls(true).Cast<MapEntity>());
}
else
{
if (args.Length > 2)
{
targetItems.AddRange(Item.ItemList.Where(item => item.Submarine == Submarine.MainSub).Where(item => item.HasTag(args[2])).Cast<MapEntity>());
}
else
{
ThrowError("Argument \"tag\" is required.");
return;
}
}
if (!targetItems.Any())
{
ThrowError("No valid items found.");
return;
}
foreach (MapEntity targetItem in targetItems)
{
Upgrade existingUpgrade = targetItem.GetUpgrade(args[0]);
if (!(targetItem is ISerializableEntity sEntity)) { continue; }
var upgrade = new Upgrade(sEntity, upgradePrefab, level);
if (targetItem.AddUpgrade(upgrade, true))
{
if (existingUpgrade == null)
{
NewMessage($"Added {upgradePrefab.Identifier}:{level} to {sEntity.Name}.", Color.Green);
upgrade.ApplyUpgrade();
}
else
{
NewMessage($"Set {sEntity.Name}'s {upgradePrefab.Identifier} upgrade to level {existingUpgrade.Level}.", Color.Cyan);
existingUpgrade.ApplyUpgrade();
}
}
else
{
ThrowError($"{upgrade.Prefab.Identifier} cannot be applied to {sEntity.Name}");
}
}
}
else
{
ThrowError("Parameter \"upgrade\" is required.");
}
}, () =>
{
return new[]
{
UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().ToArray()
};
}, true));
commands.Add(new Command("power", "power: Immediately powers up the submarine's nuclear reactor.", (string[] args) =>
{
Item reactorItem = Item.ItemList.Find(i => i.GetComponent<Reactor>() != null);
if (reactorItem == null) return;
float power = 1000.0f;
if (args.Length > 0) float.TryParse(args[0], out power);
if (reactorItem == null) { return; }
var reactor = reactorItem.GetComponent<Reactor>();
reactor.TurbineOutput = power / reactor.MaxPowerOutput * 100.0f;
reactor.FissionRate = power / reactor.MaxPowerOutput * 100.0f;
reactor.PowerOn = true;
reactor.AutoTemp = true;
reactor.PowerUpImmediately();
#if SERVER
if (GameMain.Server != null)
{
@@ -925,7 +1129,7 @@ namespace Barotrauma
{
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
};
}));
}, isCheat: true));
commands.Add(new Command("killmonsters", "killmonsters: Immediately kills all AI-controlled enemies in the level.", (string[] args) =>
{
@@ -934,7 +1138,7 @@ namespace Barotrauma
if (!(c.AIController is EnemyAIController)) continue;
c.SetAllDamage(200.0f, 0.0f, 0.0f);
}
}, null, true));
}, null, isCheat: true));
commands.Add(new Command("setclientcharacter", "setclientcharacter [client name] [character name]: Gives the client control of the specified character.", null,
() =>
@@ -1007,7 +1211,7 @@ namespace Barotrauma
commands.Add(new Command("money", "", args =>
{
if (args.Length == 0) { return; }
if (GameMain.GameSession.GameMode is CampaignMode campaign)
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
if (int.TryParse(args[0], out int money))
{
@@ -1034,6 +1238,8 @@ 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("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)
@@ -1062,21 +1268,6 @@ namespace Barotrauma
};
}));
commands.Add(new Command("debugai", "", onExecute: (string[] args) =>
{
var commands = new List<KeyValuePair<string, string[]>>()
{
new KeyValuePair<string, string[]>("debugdraw", new string[]{ "true" }),
new KeyValuePair<string, string[]>("los", new string[]{ "false" }),
new KeyValuePair<string, string[]>("lights", new string[]{ "false" }),
new KeyValuePair<string, string[]>("freecam", new string[0]),
};
foreach (var command in commands)
{
Commands.Find(c => c.names.Any(n => n.Equals(command.Key, StringComparison.OrdinalIgnoreCase)))?.Execute(command.Value);
}
}));
commands.Add(new Command("simulatedlatency", "simulatedlatency [minimumlatencyseconds] [randomlatencyseconds]: applies a simulated latency to network messages. Useful for simulating real network conditions when testing the multiplayer locally.", (string[] args) =>
{
if (args.Count() < 2 || (GameMain.NetworkMember == null)) return;
@@ -1165,7 +1356,8 @@ namespace Barotrauma
commands.Add(new Command("togglecharacternames", "Toggle the names hovering above characters on/off (client-only).", null));
commands.Add(new Command("followsub", "Toggle whether the camera should follow the nearest submarine (client-only).", null));
commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true));
commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true));
InitProjectSpecific();
commands.Sort((c1, c2) => c1.names[0].CompareTo(c2.names[0]));
@@ -1382,7 +1574,7 @@ namespace Barotrauma
private static void SpawnCharacter(string[] args, Vector2 cursorWorldPos, out string errorMsg)
{
errorMsg = "";
if (args.Length == 0) return;
if (args.Length == 0) { return; }
Character spawnedCharacter = null;
@@ -1443,9 +1635,9 @@ namespace Barotrauma
spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy);
}
if (string.IsNullOrWhiteSpace(args[0])) return;
if (string.IsNullOrWhiteSpace(args[0])) { return; }
if (spawnPoint != null) spawnPosition = spawnPoint.WorldPosition;
if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; }
if (human)
{
@@ -1454,11 +1646,8 @@ namespace Barotrauma
spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8));
if (GameMain.GameSession != null)
{
if (GameMain.GameSession.GameMode != null && !GameMain.GameSession.GameMode.IsSinglePlayer)
{
//TODO: a way to select which team to spawn to?
spawnedCharacter.TeamID = Character.Controlled != null ? Character.Controlled.TeamID : Character.TeamType.Team1;
}
//TODO: a way to select which team to spawn to?
spawnedCharacter.TeamID = Character.Controlled != null ? Character.Controlled.TeamID : Character.TeamType.Team1;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
#endif
@@ -1549,10 +1738,22 @@ namespace Barotrauma
{
var spawnedItem = new Item(itemPrefab, Vector2.Zero, null);
spawnInventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots);
onItemSpawned(spawnedItem);
}
else
{
Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnInventory);
Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned);
}
static void onItemSpawned(Item item)
{
if (item.ParentInventory?.Owner is Character character)
{
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = character.TeamID;
}
}
}
}
}
@@ -1565,13 +1766,13 @@ namespace Barotrauma
NewMessage(msg, Color.White, isCommand);
}
public static void NewMessage(string msg, Color color, bool isCommand = false)
public static void NewMessage(string msg, Color color, bool isCommand = false, bool isError = false)
{
if (string.IsNullOrEmpty(msg)) { return; }
lock (queuedMessages)
{
queuedMessages.Enqueue(new ColoredText(msg, color, isCommand));
queuedMessages.Enqueue(new ColoredText(msg, color, isCommand, isError));
}
}
@@ -1669,25 +1870,8 @@ namespace Barotrauma
}
}
System.Diagnostics.Debug.WriteLine(error);
#if CLIENT
if (listBox == null) { NewMessage(error, Color.Red); return; }
var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), style: "InnerFrame", color: Color.White)
{
CanBeFocused = false
};
var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) },
error, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true)
{
CanBeFocused = false,
TextColor = Color.Red
};
textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5);
textBlock.SetTextPos();
listBox.UpdateScrollBarSize();
listBox.BarScroll = 1.0f;
if (createMessageBox)
{
CoroutineManager.StartCoroutine(CreateMessageBox(error));
@@ -1696,8 +1880,34 @@ namespace Barotrauma
{
isOpen = true;
}
#endif
NewMessage(error, Color.Red, isError: true);
}
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(error, Color.Red);
NewMessage($"WARNING: {warning}", Color.Yellow);
#endif
}
@@ -2,7 +2,7 @@
namespace Barotrauma
{
class ArtifactEvent : ScriptedEvent
class ArtifactEvent : Event
{
private ItemPrefab itemPrefab;
@@ -14,6 +14,11 @@ namespace Barotrauma
private bool spawnPending;
public bool SpawnPending => spawnPending;
public int State => state;
public Item Item => item;
public Vector2 SpawnPos => spawnPos;
public override Vector2 DebugDrawPos
{
get { return spawnPos; }
@@ -24,7 +29,7 @@ namespace Barotrauma
return "ArtifactEvent (" + (itemPrefab == null ? "null" : itemPrefab.Name) + ")";
}
public ArtifactEvent(ScriptedEventPrefab prefab)
public ArtifactEvent(EventPrefab prefab)
: base(prefab)
{
if (prefab.ConfigElement.Attribute("itemname") != null)
@@ -0,0 +1,60 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
namespace Barotrauma
{
class Event
{
protected bool isFinished;
protected readonly EventPrefab prefab;
public EventPrefab Prefab => prefab;
public bool IsFinished
{
get { return isFinished; }
}
public override string ToString()
{
return "Event (" + prefab.EventType.ToString() +")";
}
public virtual Vector2 DebugDrawPos
{
get
{
return Vector2.Zero;
}
}
public Event(EventPrefab prefab)
{
this.prefab = prefab;
}
public virtual IEnumerable<ContentFile> GetFilesToPreload()
{
yield break;
}
public virtual void Init(bool affectSubImmediately)
{
}
public virtual void Update(float deltaTime)
{
}
public virtual void Finished()
{
isFinished = true;
}
public virtual bool CanAffectSubImmediately(Level level)
{
return true;
}
}
}
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class AfflictionAction : EventAction
{
[Serialize("", true)]
public string Affliction { get; set; }
[Serialize(0.0f, true)]
public float Strength { get; set; }
[Serialize(LimbType.None, true)]
public LimbType LimbType { get; set; }
[Serialize("", true)]
public string TargetTag { get; set; }
public AfflictionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
var afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(p => p.Identifier.Equals(Affliction, StringComparison.InvariantCultureIgnoreCase));
if (afflictionPrefab != null)
{
var targets = ParentEvent.GetTargets(TargetTag);
foreach (var target in targets)
{
if (target != null && target is Character character)
{
var limb = LimbType != LimbType.None ? character.AnimController.GetLimb(LimbType) : null;
if (Strength > 0.0f)
{
character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength));
}
else if (Strength < 0.0f)
{
character.CharacterHealth.ReduceAffliction(limb, Affliction, -Strength);
}
}
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(AfflictionAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"Affliction: {Affliction.ColorizeObject()}, Strength: {Strength.ColorizeObject()}, " +
$"LimbType: {LimbType.ColorizeObject()})";
}
}
}
@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
abstract class BinaryOptionAction : EventAction
{
public SubactionGroup Success = null;
public SubactionGroup Failure = null;
protected bool? succeeded = null;
public BinaryOptionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
foreach (XElement elem in element.Elements())
{
string elemName = elem.Name.LocalName;
if (elemName.Equals("success", StringComparison.InvariantCultureIgnoreCase))
{
Success ??= new SubactionGroup(ParentEvent, elem);
}
else if (elemName.Equals("failure", StringComparison.InvariantCultureIgnoreCase))
{
Failure ??= new SubactionGroup(ParentEvent, elem);
}
}
}
public override IEnumerable<EventAction> GetSubActions()
{
IEnumerable<EventAction> actions = Success?.Actions ?? Enumerable.Empty<EventAction>();
actions = actions.Concat(Failure?.Actions ?? Enumerable.Empty<EventAction>());
return actions;
}
public override bool IsFinished(ref string goTo)
{
return DetermineFinished(ref goTo);
}
protected bool DetermineFinished()
{
string throwaway = null;
return DetermineFinished(ref throwaway);
}
protected bool DetermineFinished(ref string goTo)
{
if (succeeded.HasValue)
{
if (succeeded.Value)
{
if (Success == null || Success.IsFinished(ref goTo))
{
return true;
}
}
else
{
if (Failure == null || Failure.IsFinished(ref goTo))
{
return true;
}
}
}
return false;
}
public override bool SetGoToTarget(string goTo)
{
if (Success != null && Success.SetGoToTarget(goTo))
{
succeeded = true;
return true;
}
else if (Failure != null && Failure.SetGoToTarget(goTo))
{
succeeded = false;
return true;
}
return false;
}
public override void Reset()
{
Success?.Reset();
Failure?.Reset();
succeeded = null;
}
public override void Update(float deltaTime)
{
if (succeeded.HasValue)
{
if (succeeded.Value)
{
Success?.Update(deltaTime);
}
else
{
Failure?.Update(deltaTime);
}
}
else
{
succeeded = DetermineSuccess();
}
}
protected abstract bool? DetermineSuccess();
}
}
@@ -0,0 +1,126 @@
#nullable enable
using System.Xml.Linq;
namespace Barotrauma
{
class CheckDataAction : BinaryOptionAction
{
[Serialize("", true)]
public string Identifier { get; set; } = null!;
[Serialize("", true)]
public string Condition { get; set; } = null!;
protected object? value2;
protected object? value1;
protected PropertyConditional.OperatorType Operator { get; set; }
public CheckDataAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
if (!(GameMain.GameSession?.GameMode is CampaignMode campaignMode)) { return false; }
string[] splitString = Condition.Split(' ');
string value = Condition;
if (splitString.Length > 0)
{
for (int i = 1; i < splitString.Length; i++)
{
value = splitString[i] + (i > 1 && i < splitString.Length ? " " : "");
}
}
else
{
DebugConsole.ThrowError($"{Condition} is too short, it should start with an operator followed by a boolean or a floating point value.");
return false;
}
string op = splitString[0];
Operator = PropertyConditional.GetOperatorType(op);
if (Operator == PropertyConditional.OperatorType.None) { return false; }
bool? tryBoolean = TryBoolean(campaignMode, value);
if (tryBoolean != null) { return tryBoolean; }
bool? tryFloat = TryFloat(campaignMode, value);
if (tryFloat != null) { return tryFloat; }
DebugConsole.ThrowError($"{value2} ({Condition}) did not match a boolean or a float.");
return false;
}
private bool? TryBoolean(CampaignMode campaignMode, string value)
{
if (bool.TryParse(value, out bool b))
{
bool target = GetBool(campaignMode);
value1 = target;
value2 = b;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
return target == b;
case PropertyConditional.OperatorType.NotEquals:
return target != b;
default:
DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a boolean (was {Operator} for {value}).");
return false;
}
}
DebugConsole.Log($"{value} != bool");
return null;
}
private bool? TryFloat(CampaignMode campaignMode, string value)
{
if (float.TryParse(value, out float f))
{
float target = GetFloat(campaignMode);
value1 = target;
value2 = f;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
return MathUtils.NearlyEqual(target, f);
case PropertyConditional.OperatorType.GreaterThan:
return target > f;
case PropertyConditional.OperatorType.GreaterThanEquals:
return target >= f;
case PropertyConditional.OperatorType.LessThan:
return target < f;
case PropertyConditional.OperatorType.LessThanEquals:
return target <= f;
case PropertyConditional.OperatorType.NotEquals:
return !MathUtils.NearlyEqual(target, f);
}
}
DebugConsole.Log($"{value} != float");
return null;
}
protected virtual bool GetBool(CampaignMode campaignMode)
{
return campaignMode.CampaignMetadata.GetBoolean(Identifier);
}
protected virtual float GetFloat(CampaignMode campaignMode)
{
return campaignMode.CampaignMetadata.GetFloat(Identifier);
}
public override string ToDebugString()
{
string condition = "?";
if (value2 != null && value1 != null)
{
condition = $"{value1.ColorizeObject()} {Operator.ColorizeObject()} {value2.ColorizeObject()}";
}
return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckDataAction)} -> (Data: {Identifier.ColorizeObject()}, Success: {succeeded.ColorizeObject()}, Expression: {condition})";
}
}
}
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class CheckItemAction : BinaryOptionAction
{
[Serialize("", true)]
public string TargetTag { get; set; }
[Serialize("", true)]
public string ItemIdentifiers { get; set; }
[Serialize("", true)]
public string ItemTags { get; set; }
private readonly string[] itemIdentifierSplit;
private readonly string[] itemTags;
public CheckItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
itemIdentifierSplit = ItemIdentifiers.Split(',');
itemTags = ItemTags.Split(",");
}
protected override bool? DetermineSuccess()
{
var targets = ParentEvent.GetTargets(TargetTag);
if (!targets.Any()) { return null; }
foreach (var target in targets)
{
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; }
foreach (var identifier in itemIdentifierSplit)
{
if (chr.Inventory.Items.Any(it => it != null && it.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)))
{
return true;
}
}
}
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(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}" +
$"Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" +
subActionStr;
}
}
}
@@ -0,0 +1,61 @@
#nullable enable
using System;
using System.Diagnostics;
using System.Xml.Linq;
namespace Barotrauma
{
class CheckReputationAction : CheckDataAction
{
[Serialize(ReputationAction.ReputationType.None, true)]
public ReputationAction.ReputationType TargetType { get; set; }
public CheckReputationAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
protected override float GetFloat(CampaignMode campaignMode)
{
switch (TargetType)
{
case ReputationAction.ReputationType.Faction:
{
Faction? faction = campaignMode.Factions.Find(f => f.Prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase));
if (faction != null) { return faction.Reputation.Value; }
break;
}
case ReputationAction.ReputationType.Location:
{
Location? location = campaignMode.Map.CurrentLocation;
Debug.Assert(location?.Reputation != null, "location?.Reputation != null");
if (location?.Reputation != null) { return location.Reputation.Value; }
break;
}
default:
{
DebugConsole.ThrowError("CheckReputationAction requires a \"TargetType\" but none were specified.");
break;
}
}
return 0.0f;
}
protected override bool GetBool(CampaignMode campaignMode)
{
DebugConsole.ThrowError("Boolean comparison cannot be applied to reputations.");
return false;
}
public override string ToDebugString()
{
string condition = "?";
if (value2 != null && value1 != null)
{
condition = $"{value1.ColorizeObject()} {Operator.ColorizeObject()} {value2.ColorizeObject()}";
}
return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckReputationAction)} -> (Type: {TargetType.ColorizeObject()}, " +
$"{(string.IsNullOrWhiteSpace(Identifier) ? string.Empty : $"Identifier: {Identifier.ColorizeObject()}, ")}" +
$"Success: {succeeded.ColorizeObject()}, Expression: {condition})";
}
}
}
@@ -0,0 +1,91 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class CombatAction : EventAction
{
[Serialize(AIObjectiveCombat.CombatMode.Offensive, true)]
public AIObjectiveCombat.CombatMode CombatMode { get; set; }
[Serialize("", true)]
public string NPCTag { get; set; }
[Serialize("", true)]
public string EnemyTag { get; set; }
[Serialize(120.0f, true)]
public float CoolDown { get; set; }
private bool isFinished = false;
private IEnumerable<Character> affectedNpcs = null;
public CombatAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
public override void Update(float deltaTime)
{
if (isFinished) { return; }
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(e => e is Character).Select(e => e as Character);
foreach (var npc in affectedNpcs)
{
if (!(npc.AIController is HumanAIController humanAiController)) { continue; }
Character enemy = null;
float closestDist = float.MaxValue;
foreach (Entity target in ParentEvent.GetTargets(EnemyTag))
{
if (!(target is Character character)) { continue; }
float dist = Vector2.DistanceSquared(npc.WorldPosition, target.WorldPosition);
if (dist < closestDist)
{
enemy = character;
closestDist = dist;
}
}
if (enemy == null) { continue; }
npc.TurnedHostileByEvent = true;
var objectiveManager = humanAiController.ObjectiveManager;
foreach (var goToObjective in objectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
{
goToObjective.Abandon = true;
}
objectiveManager.AddObjective(new AIObjectiveCombat(npc, enemy, CombatMode, objectiveManager, coolDown: CoolDown));
}
isFinished = true;
}
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
if (affectedNpcs != null)
{
foreach (var npc in affectedNpcs)
{
if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; }
foreach (var combatObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveCombat>())
{
combatObjective.Abandon = true;
}
}
affectedNpcs = null;
}
isFinished = false;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(CombatAction)} -> (Cooldown: {CoolDown.ColorizeObject()}, CombatMode: {CombatMode.ColorizeObject()}, NPCTag: {NPCTag.ColorizeObject()}, EnemyTag: {EnemyTag.ColorizeObject()})";
}
}
}
@@ -0,0 +1,359 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class ConversationAction : EventAction
{
public enum DialogTypes
{
Regular,
Small,
Mission
}
const float InterruptDistance = 300.0f;
/// <summary>
/// Other events can't trigger conversations if some other event has triggered one within this time.
/// Intended to prevent multiple events from triggering conversations at the same time.
/// </summary>
const float BlockOtherConversationsDuration = 5.0f;
[Serialize("", true)]
public string Text { get; set; }
[Serialize(0, true)]
public int DefaultOption { get; set; }
[Serialize("", true)]
public string SpeakerTag { get; set; }
[Serialize("", true)]
public string TargetTag { get; set; }
[Serialize(true, true)]
public bool WaitForInteraction { get; set; }
[Serialize(false, true)]
public bool FadeToBlack { get; set; }
[Serialize("", true)]
public string EventSprite { get; set; }
[Serialize(DialogTypes.Regular, true)]
public DialogTypes DialogType { get; set; }
[Serialize(false, true)]
public bool ContinueConversation { get; set; }
private Character speaker;
private OrderInfo? prevSpeakerOrder;
public List<SubactionGroup> Options { get; private set; }
public SubactionGroup Interrupted { get; private set; }
private static UInt16 actionCount;
//an identifier the server uses to identify which ConversationAction a client is responding to
public readonly UInt16 Identifier;
private int selectedOption = -1;
private bool dialogOpened = false;
private double lastActiveTime;
private bool interrupt;
public ConversationAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
actionCount++;
Identifier = actionCount;
Options = new List<SubactionGroup>();
foreach (XElement elem in element.Elements())
{
if (elem.Name.LocalName.Equals("option", StringComparison.InvariantCultureIgnoreCase))
{
Options.Add(new SubactionGroup(ParentEvent, elem));
}
else if (elem.Name.LocalName.Equals("interrupt", StringComparison.InvariantCultureIgnoreCase))
{
Interrupted = new SubactionGroup(ParentEvent, elem);
}
}
}
public override IEnumerable<EventAction> GetSubActions()
{
return Options.SelectMany(group => group.Actions);
}
public override bool IsFinished(ref string goTo)
{
if (interrupt)
{
if (dialogOpened)
{
#if CLIENT
dialogBox?.Close();
#else
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.InGame && c.Character != null) { ServerWrite(speaker, c); }
}
# endif
ResetSpeaker();
dialogOpened = false;
}
if (Interrupted == null)
{
goTo = "_end";
return true;
}
else
{
return Interrupted.IsFinished(ref goTo);
}
}
if (selectedOption >= 0)
{
if (!Options.Any() || Options[selectedOption].IsFinished(ref goTo))
{
ResetSpeaker();
return true;
}
}
return false;
}
public override void Reset()
{
Options.ForEach(a => a.Reset());
ResetSpeaker();
selectedOption = -1;
interrupt = false;
dialogOpened = false;
speaker = null;
}
public override bool SetGoToTarget(string goTo)
{
selectedOption = -1;
for (int i = 0; i < Options.Count; i++)
{
if (Options[i].SetGoToTarget(goTo))
{
selectedOption = i;
interrupt = false;
dialogOpened = true;
return true;
}
}
return false;
}
private void ResetSpeaker()
{
if (speaker == null) { return; }
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
speaker.SetCustomInteract(null, null);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
#endif
if (prevSpeakerOrder != 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);
}
}
private int[] GetEndingOptions()
{
List<int> endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList();
if (!ContinueConversation) { endings.Add(-1); }
return endings.ToArray();
}
public override void Update(float deltaTime)
{
lastActiveTime = Timing.TotalTime;
if (interrupt)
{
Interrupted?.Update(deltaTime);
}
else if (selectedOption < 0)
{
if (dialogOpened)
{
#if CLIENT
Character.DisableControls = true;
#endif
if (ShouldInterrupt())
{
ResetSpeaker();
interrupt = true;
}
return;
}
if (!string.IsNullOrEmpty(SpeakerTag))
{
if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk) { return; }
speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character;
if (speaker == null || speaker.Removed)
{
return;
}
//some conversation already assigned to the speaker, wait for it to be removed
if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk)
{
return;
}
else if (!WaitForInteraction)
{
TryStartConversation(speaker);
}
else
{
speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk;
#if CLIENT
speaker.SetCustomInteract(
TryStartConversation,
TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameMain.Config.KeyBindText(InputType.Use)));
#else
speaker.SetCustomInteract(
TryStartConversation,
TextManager.Get("CampaignInteraction.Talk"));
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
#endif
}
return;
}
else
{
TryStartConversation(null);
}
}
else if (Options.Any())
{
Options[selectedOption].Update(deltaTime);
}
}
private bool ShouldInterrupt()
{
IEnumerable<Entity> targets = Enumerable.Empty<Entity>();
if (!string.IsNullOrEmpty(TargetTag))
{
targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e));
if (!targets.Any()) { return true; }
}
if (speaker != null)
{
if (!string.IsNullOrEmpty(TargetTag))
{
if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; }
}
if (speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction())
{
return true;
}
return speaker.Removed || speaker.IsDead || speaker.IsIncapacitated;
}
return false;
}
private bool IsValidTarget(Entity e)
{
return
e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
(e == Character.Controlled || character.IsRemotePlayer);
}
private void TryStartConversation(Character speaker, Character targetCharacter = null)
{
IEnumerable<Entity> targets = Enumerable.Empty<Entity>();
if (!string.IsNullOrEmpty(TargetTag))
{
targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e));
if (!targets.Any() || IsBlockedByAnotherConversation(targets)) { return; }
}
if (speaker?.AIController is HumanAIController humanAI)
{
prevSpeakerOrder = null;
if (humanAI.CurrentOrder != null)
{
prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption);
}
humanAI.SetOrder(
Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)),
option: string.Empty, orderGiver: null, speak: false);
if (targets.Any())
{
Entity closestTarget = null;
float closestDist = float.MaxValue;
foreach (Entity entity in targets)
{
float dist = Vector2.DistanceSquared(entity.WorldPosition, speaker.WorldPosition);
if (dist < closestDist)
{
closestTarget = entity;
closestDist = dist;
}
}
if (closestTarget != null)
{
humanAI.FaceTarget(closestTarget);
}
}
}
ShowDialog(speaker, targetCharacter);
dialogOpened = true;
}
partial void ShowDialog(Character speaker, Character targetCharacter);
public override string ToDebugString()
{
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()}";
}
else
{
return $"{ToolBox.GetDebugSymbol(true)} {nameof(ConversationAction)} -> (Interrupted)\n" +
$" Sub action: {Interrupted?.CurrentSubAction.ColorizeObject()}";
}
}
}
}
@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
abstract class EventAction
{
public class SubactionGroup
{
public string Text;
public List<EventAction> Actions;
public bool EndConversation;
private int currentSubAction = 0;
public EventAction CurrentSubAction
{
get
{
if (currentSubAction >= 0 && Actions.Count > currentSubAction)
{
return Actions[currentSubAction];
}
return null;
}
}
public SubactionGroup(ScriptedEvent scriptedEvent, XElement elem)
{
Text = elem.Attribute("text")?.Value ?? "";
Actions = new List<EventAction>();
EndConversation = elem.GetAttributeBool("endconversation", false);
foreach (XElement e in elem.Elements())
{
if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
{
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction.");
continue;
}
Actions.Add(Instantiate(scriptedEvent, e));
}
}
public bool IsFinished(ref string goTo)
{
if (currentSubAction < Actions.Count)
{
string innerGoTo = null;
if (Actions[currentSubAction].IsFinished(ref innerGoTo))
{
if (string.IsNullOrEmpty(innerGoTo))
{
currentSubAction++;
}
else
{
goTo = innerGoTo;
return true;
}
}
}
if (currentSubAction >= Actions.Count)
{
return true;
}
return false;
}
public bool SetGoToTarget(string goTo)
{
currentSubAction = 0;
for (int i = 0; i < Actions.Count; i++)
{
if (Actions[i].SetGoToTarget(goTo))
{
currentSubAction = i;
return true;
}
}
return false;
}
public void Reset()
{
Actions.ForEach(a => a.Reset());
currentSubAction = 0;
}
public void Update(float deltaTime)
{
if (currentSubAction < Actions.Count)
{
Actions[currentSubAction].Update(deltaTime);
}
}
}
public readonly ScriptedEvent ParentEvent;
public EventAction(ScriptedEvent parentEvent, XElement element)
{
ParentEvent = parentEvent;
SerializableProperty.DeserializeProperties(this, element);
}
/// <summary>
/// Has the action finished.
/// </summary>
/// <param name="goToLabel">If null or empty, the event moves to the next action. Otherwise it moves to the specified label.</param>
/// <returns></returns>
public abstract bool IsFinished(ref string goToLabel);
public virtual bool SetGoToTarget(string goTo)
{
return false;
}
public abstract void Reset();
public virtual bool CanBeFinished()
{
return true;
}
public virtual IEnumerable<EventAction> GetSubActions()
{
return Enumerable.Empty<EventAction>();
}
public virtual void Update(float deltaTime) { }
public static EventAction Instantiate(ScriptedEvent scriptedEvent, XElement element)
{
Type actionType = null;
try
{
actionType = Type.GetType("Barotrauma." + element.Name, true, true);
if (actionType == null) { throw new NullReferenceException(); }
}
catch
{
DebugConsole.ThrowError("Could not find an event class of the type \"" + element.Name + "\".");
return null;
}
ConstructorInfo constructor = actionType.GetConstructor(new[] { typeof(ScriptedEvent), typeof(XElement) });
try
{
return constructor.Invoke(new object[] { scriptedEvent, element }) as EventAction;
}
catch (Exception ex)
{
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
return null;
}
}
/// <summary>
/// Rich test to display in debugdraw
/// </summary>
/// <example>
/// <code>
/// public override string ToDebugString()
/// {
/// return $"{ToolBox.GetDebugSymbol(isFinished)} SomeAction -> "(someInfo: {info.ColorizeObject()})";
/// }
/// </code>
/// </example>
/// <returns></returns>
public virtual string ToDebugString()
{
return $"[?] {GetType().Name}";
}
}
}
@@ -0,0 +1,50 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class FireAction : EventAction
{
[Serialize(10.0f, true)]
public float Size { get; set; }
[Serialize("", true)]
public string TargetTag { get; set; }
public FireAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
var targets = ParentEvent.GetTargets(TargetTag);
foreach (var target in targets)
{
Vector2 pos = target.WorldPosition;
var newFire = new FireSource(pos);
newFire.Size = new Vector2(Size, Size);
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(FireAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"Size: {Size.ColorizeObject()})";
}
}
}
@@ -0,0 +1,56 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class GiveSkillExpAction : EventAction
{
[Serialize("", true)]
public string Skill { get; set; }
[Serialize(0.0f, true)]
public float Amount { get; set; }
[Serialize("", true)]
public string TargetTag { get; set; }
public GiveSkillExpAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
if (string.IsNullOrEmpty(TargetTag))
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": GiveSkillExpAction without a target tag (the action needs to know whose skill to check).");
}
}
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
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);
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(GiveSkillExpAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"Skill: {Skill.ColorizeObject()}, Amount: {Amount.ColorizeObject()})";
}
}
}
@@ -0,0 +1,25 @@
using System.Xml.Linq;
namespace Barotrauma
{
class GoTo : EventAction
{
[Serialize("", true)]
public string Name { get; set; }
public GoTo(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
public override bool IsFinished(ref string goTo)
{
goTo = Name;
return true;
}
public override string ToDebugString()
{
return $"[-] Go to label \"{Name}\"";
}
public override void Reset() { }
}
}
@@ -0,0 +1,29 @@
using System.Xml.Linq;
namespace Barotrauma
{
class Label : EventAction
{
[Serialize("", true)]
public string Name { get; set; }
public Label(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
public override bool IsFinished(ref string goTo)
{
return true;
}
public override bool SetGoToTarget(string goTo)
{
return goTo.Equals(Name, System.StringComparison.InvariantCultureIgnoreCase);
}
public override string ToDebugString()
{
return $"[-] Label \"{Name}\"";
}
public override void Reset() { }
}
}
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class MissionAction : EventAction
{
[Serialize("", true)]
public string MissionIdentifier { get; set; }
[Serialize("", true)]
public string MissionTag { get; set; }
private bool isFinished;
public MissionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
//TODO: use event identifier in the error messages
if (string.IsNullOrEmpty(MissionIdentifier) && string.IsNullOrEmpty(MissionTag))
{
DebugConsole.ThrowError($"Error in event \"{"event identifier goes here"}\": neither MissionIdentifier or MissionTag has been configured.");
}
if (!string.IsNullOrEmpty(MissionIdentifier) && !string.IsNullOrEmpty(MissionTag))
{
DebugConsole.ThrowError($"Error in event \"{"event identifier goes here"}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored.");
}
}
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (GameMain.GameSession.GameMode is CampaignMode campaign)
{
MissionPrefab prefab = null;
if (!string.IsNullOrEmpty(MissionIdentifier))
{
prefab = campaign.Map.CurrentLocation.UnlockMissionByIdentifier(MissionIdentifier);
}
else if (!string.IsNullOrEmpty(MissionTag))
{
prefab = campaign.Map.CurrentLocation.UnlockMissionByTag(MissionTag);
}
if (campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.LastUpdateID++;
}
if (prefab != null)
{
#if CLIENT
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name),
new string[0], type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128))
{
IconColor = prefab.IconColor
};
#else
NotifyMissionUnlock(prefab);
#endif
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(string.IsNullOrEmpty(MissionIdentifier) ? MissionTag : MissionIdentifier)})";
}
#if SERVER
private void NotifyMissionUnlock(MissionPrefab prefab)
{
foreach (Client client in GameMain.Server.ConnectedClients)
{
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.Write((byte) ServerPacketHeader.EVENTACTION);
outmsg.Write((byte) EventManager.NetworkEventType.MISSION);
outmsg.Write(prefab.Identifier);
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
}
#endif
}
}
@@ -0,0 +1,44 @@
using System;
using System.Xml.Linq;
namespace Barotrauma
{
class MoneyAction : EventAction
{
public MoneyAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
[Serialize(0, true)]
public int Amount { get; set; }
private bool isFinished;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
campaign.Money += Amount;
#if SERVER
(campaign as MultiPlayerCampaign).LastUpdateID++;
#endif
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetDataAction)} -> (Amount: {Amount.ColorizeObject()})";
}
}
}
@@ -0,0 +1,94 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class NPCFollowAction : EventAction
{
[Serialize("", true)]
public string NPCTag { get; set; }
[Serialize("", true)]
public string TargetTag { get; set; }
[Serialize(true, true)]
public bool Follow { get; set; }
private bool isFinished = false;
public NPCFollowAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
private List<Character> affectedNpcs = null;
private Entity target = null;
public override void Update(float deltaTime)
{
if (isFinished) { return; }
target = ParentEvent.GetTargets(TargetTag).FirstOrDefault();
if (target == null) { return; }
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
foreach (var npc in affectedNpcs)
{
if (!(npc.AIController is HumanAIController humanAiController)) { continue; }
if (Follow)
{
var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f
};
humanAiController.ObjectiveManager.AddObjective(newObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
}
else
{
foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
{
if (goToObjective.Target == target)
{
goToObjective.Abandon = true;
}
}
}
}
isFinished = true;
}
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
if (affectedNpcs != null && target != null)
{
foreach (var npc in affectedNpcs)
{
if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; }
foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
{
if (goToObjective.Target == target)
{
goToObjective.Abandon = true;
}
}
}
target = null;
affectedNpcs = null;
}
isFinished = false;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(NPCFollowAction)} -> (NPCTag: {NPCTag.ColorizeObject()}, TargetTag: {TargetTag.ColorizeObject()}, Follow: {Follow.ColorizeObject()})";
}
}
}
@@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class NPCWaitAction : EventAction
{
[Serialize("", true)]
public string NPCTag { get; set; }
[Serialize(true, true)]
public bool Wait { get; set; }
private bool isFinished = false;
public NPCWaitAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
private List<Character> affectedNpcs = null;
public override void Update(float deltaTime)
{
if (isFinished) { return; }
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
foreach (var npc in affectedNpcs)
{
if (!(npc.AIController is HumanAIController humanAiController)) { continue; }
if (Wait)
{
var newObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f
};
humanAiController.ObjectiveManager.AddObjective(newObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
}
else
{
foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
{
if (goToObjective.Target == npc)
{
goToObjective.Abandon = true;
}
}
}
}
isFinished = true;
}
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
if (affectedNpcs != null)
{
foreach (var npc in affectedNpcs)
{
if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; }
foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
{
if (goToObjective.Target == npc)
{
goToObjective.Abandon = true;
}
}
}
affectedNpcs = null;
}
isFinished = false;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(NPCWaitAction)} -> (NPCTag: {NPCTag.ColorizeObject()}, Wait: {Wait.ColorizeObject()})";
}
}
}
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
class RNGAction : BinaryOptionAction
{
[Serialize(0.0f, true)]
public float Chance { get; set; }
public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
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;
}
}
}
@@ -0,0 +1,58 @@
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class RemoveItemAction : EventAction
{
[Serialize("", true)]
public string TargetTag { get; set; }
[Serialize("", true)]
public string ItemIdentifier { get; set; }
[Serialize(1, true)]
public int Amount { get; set; }
public RemoveItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
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 items = targets[0].Inventory.Items;
for (int i = 0; i < items.Length; i++)
{
if (items[i] != null && items[i].Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase))
{
Entity.Spawner.AddToRemoveQueue(items[i]);
count--;
if (count <= 0) { break; }
}
}
targets.RemoveAt(0);
}
isFinished = true;
}
}
}
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class ReputationAction : EventAction
{
public enum ReputationType
{
None,
Location,
Faction
}
public ReputationAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
[Serialize(0.0f, true)]
public float Increase { get; set; }
[Serialize("", true)]
public string Identifier { get; set; }
[Serialize(ReputationType.None, true)]
public ReputationType TargetType { get; set; }
private bool isFinished;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
switch (TargetType)
{
case ReputationType.Faction:
{
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase));
if (faction != null)
{
faction.Reputation.Value += Increase;
}
else
{
DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found.");
}
break;
}
case ReputationType.Location:
{
Location location = campaign.Map.CurrentLocation;
if (location != null)
{
location.Reputation.Value += Increase;
IEnumerable<Location> locations = location.Connections.SelectMany(c => c.Locations).Distinct().Where(l => l != null && l != location);
foreach (Location connectedLocation in locations)
{
Debug.Assert(connectedLocation.Reputation != null, "connectedLocation.Reputation != null");
if (connectedLocation.Reputation != null)
{
connectedLocation.Reputation.Value += (Increase / 4);
}
}
}
break;
}
default:
{
DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified.");
break;
}
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ReputationAction)} -> (FactionIdentifier: {Identifier.ColorizeObject()}, TargetType: {TargetType.ColorizeObject()}, Increase: {Increase.ColorizeObject()})";
}
}
}
@@ -0,0 +1,105 @@
using System;
using System.Xml.Linq;
namespace Barotrauma
{
class SetDataAction : EventAction
{
public enum OperationType
{
Set,
Multiply,
Add
}
public SetDataAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
[Serialize(OperationType.Set, true)]
public OperationType Operation { get; set; }
[Serialize(null, true)]
public string Value { get; set; } = null!;
[Serialize("", true)]
public string Identifier { get; set; }
private bool isFinished;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
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;
}
}
}
isFinished = true;
}
private static float? ConvertValueToFloat(object value)
{
if (value is float || value is int)
{
return (float?) Convert.ChangeType(value, typeof(float));
}
return null;
}
private object ConvertXMLValue()
{
if (bool.TryParse(Value, out bool b))
{
return b;
}
if (float.TryParse(Value, out float f))
{
return f;
}
return Value;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetDataAction)} -> (Identifier: {Identifier.ColorizeObject()}, Value: {ConvertXMLValue().ColorizeObject()}, Operation: {Operation.ColorizeObject()})";
}
}
}
@@ -0,0 +1,105 @@
using System;
using System.Xml.Linq;
namespace Barotrauma
{
class SetPriceMultiplierAction : EventAction
{
public enum OperationType
{
Set,
Multiply,
Min,
Max
}
public enum PriceMultiplierType
{
Store,
Mechanical
}
[Serialize(1.0f, true)]
public float Multiplier { get; set; }
[Serialize(OperationType.Set, true)]
public OperationType Operation { get; set; }
[Serialize(PriceMultiplierType.Store, true)]
public PriceMultiplierType TargetMultiplier { get; set; }
public SetPriceMultiplierAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.Map?.CurrentLocation != null)
{
float newMultiplier = GetCurrentMultiplier(campaign.Map.CurrentLocation);
switch (Operation)
{
case OperationType.Set:
newMultiplier = Multiplier;
break;
case OperationType.Multiply:
newMultiplier *= Multiplier;
break;
case OperationType.Min:
newMultiplier = Math.Min(Multiplier, campaign.Map.CurrentLocation.PriceMultiplier);
break;
case OperationType.Max:
newMultiplier = Math.Max(Multiplier, campaign.Map.CurrentLocation.PriceMultiplier);
break;
default:
throw new NotImplementedException();
}
SetCurrentMultiplier(campaign.Map.CurrentLocation, newMultiplier);
}
isFinished = true;
}
private float GetCurrentMultiplier(Location location)
{
return TargetMultiplier switch
{
PriceMultiplierType.Store => location.PriceMultiplier,
PriceMultiplierType.Mechanical => location.MechanicalPriceMultiplier,
_ => throw new NotImplementedException()
};
}
private void SetCurrentMultiplier(Location location, float value)
{
switch (TargetMultiplier)
{
case PriceMultiplierType.Store:
location.PriceMultiplier = value;
break;
case PriceMultiplierType.Mechanical:
location.MechanicalPriceMultiplier = value;
break;
default:
throw new NotImplementedException();
}
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetPriceMultiplierAction)} -> (Multiplier: {Multiplier.ColorizeObject()}, " +
$"Operation: {Operation.ColorizeObject()}, Target: {TargetMultiplier})";
}
}
}
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class SkillCheckAction : BinaryOptionAction
{
[Serialize("", true)]
public string RequiredSkill { get; set; }
[Serialize(0.0f, true)]
public float RequiredLevel { get; set; }
[Serialize("", true)]
public string TargetTag { get; set; }
public SkillCheckAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
if (string.IsNullOrEmpty(TargetTag))
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": SkillCheckAction without a target tag (the action needs to know whose skill to check).");
}
}
protected override bool? DetermineSuccess()
{
var potentialTargets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character);
return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill?.ToLowerInvariant()) >= RequiredLevel);
}
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;
}
}
}
@@ -0,0 +1,335 @@
using System;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class SpawnAction : EventAction
{
public enum SpawnLocationType
{
MainSub,
Outpost,
MainPath,
Ruin,
Wreck
}
[Serialize("", true, description: "Species name of the character to spawn.")]
public string SpeciesName { get; set; }
[Serialize("", true, description: "Identifier of the NPC set to choose from.")]
public string NPCSetIdentifier { get; set; }
[Serialize("", true, description: "Identifier of the NPC.")]
public string NPCIdentifier { get; set; }
[Serialize(true, true, description: "Should taking the items of this npc be considered as stealing?")]
public bool LootingIsStealing { get; set; }
[Serialize("", true, description: "Identifier of the item to spawn.")]
public string ItemIdentifier { get; set; }
[Serialize("", true, description: "The spawned entity will be assigned this tag. The tag can be used to refer to the entity by other actions of the event.")]
public string TargetTag { get; set; }
[Serialize("", true, description: "Tag of an entity with an inventory to spawn the item into.")]
public string TargetInventory { get; set; }
[Serialize(SpawnLocationType.MainSub, true)]
public SpawnLocationType SpawnLocation { get; set; }
[Serialize(SpawnType.Human, true)]
public SpawnType SpawnPointType { get; set; }
[Serialize("", true)]
public string SpawnPointTag { get; set; }
private readonly HashSet<string> targetModuleTags = new HashSet<string>();
[Serialize("", true, "What outpost module tags does the entity prefer to spawn in.")]
public string TargetModuleTags
{
get => string.Join(",", targetModuleTags);
set
{
targetModuleTags.Clear();
if (!string.IsNullOrWhiteSpace(value))
{
string[] splitTags = value.Split(',');
foreach (var s in splitTags)
{
targetModuleTags.Add(s);
}
}
}
}
private bool spawned;
private Entity spawnedEntity;
private readonly bool ignoreSpawnPointType;
public SpawnAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
ignoreSpawnPointType = !element.Attributes().Any(a => a.Name.ToString().Equals("spawnpointtype", StringComparison.OrdinalIgnoreCase));
}
public override bool IsFinished(ref string goTo)
{
if (spawnedEntity != null)
{
return true;
}
else
{
return false;
}
}
public override void Reset()
{
spawned = false;
spawnedEntity = null;
}
public override void Update(float deltaTime)
{
if (spawned) { return; }
if (!string.IsNullOrEmpty(NPCSetIdentifier) && !string.IsNullOrEmpty(NPCIdentifier))
{
HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier);
ISpatialEntity spawnPos = GetSpawnPos();
Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter =>
{
newCharacter.TeamID = Character.TeamType.FriendlyNPC;
newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine);
if (LootingIsStealing)
{
foreach (Item item in newCharacter.Inventory.Items)
{
if (item != null) { item.SpawnedInOutpost = true; }
}
}
newCharacter.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier;
var humanAI = newCharacter.AIController as HumanAIController;
if (humanAI != null)
{
var idleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
if (idleObjective != null)
{
idleObjective.Behavior = humanPrefab.BehaviorType;
foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes)
{
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
}
}
}
if (humanPrefab.CampaignInteractionType != CampaignMode.InteractionType.None)
{
(GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(newCharacter, humanPrefab.CampaignInteractionType);
if (spawnPos != null && humanAI != null)
{
humanAI.ObjectiveManager.SetOrder(new AIObjectiveGoTo(spawnPos, newCharacter, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200));
}
}
if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
});
}
else if (!string.IsNullOrEmpty(SpeciesName))
{
Entity.Spawner.AddToSpawnQueue(SpeciesName, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter =>
{
if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
});
}
else if (!string.IsNullOrEmpty(ItemIdentifier))
{
if (!(MapEntityPrefab.Find(null, identifier: ItemIdentifier) is ItemPrefab itemPrefab))
{
DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)");
}
else
{
Inventory spawnInventory = null;
if (!string.IsNullOrEmpty(TargetInventory))
{
var targets = ParentEvent.GetTargets(TargetInventory);
if (targets.Any())
{
var target = targets.First(t => t is Item || t is Character);
if (target is Character character)
{
spawnInventory = character.Inventory;
}
else if (target is Item item)
{
spawnInventory = item.OwnInventory;
}
}
if (spawnInventory == null)
{
DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\"");
}
}
if (spawnInventory == null)
{
Entity.Spawner.AddToSpawnQueue(itemPrefab, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawned: onSpawned);
}
else
{
Entity.Spawner.AddToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned);
}
void onSpawned(Item newItem)
{
if (!string.IsNullOrEmpty(TargetTag) && newItem != null)
{
ParentEvent.AddTarget(TargetTag, newItem);
}
spawnedEntity = newItem;
}
}
}
spawned = true;
}
public static Vector2 OffsetSpawnPos(Vector2 pos, float offsetAmount)
{
Hull hull = Hull.FindHull(pos);
pos += Rand.Vector(offsetAmount);
if (hull != null)
{
float margin = 50.0f;
pos = new Vector2(
MathHelper.Clamp(pos.X, hull.WorldRect.X + margin, hull.WorldRect.Right - margin),
MathHelper.Clamp(pos.Y, hull.WorldRect.Y - hull.WorldRect.Height + margin, hull.WorldRect.Y - margin));
}
return pos;
}
private ISpatialEntity GetSpawnPos()
{
if (!string.IsNullOrWhiteSpace(SpawnPointTag))
{
List<Item> potentialItems = SpawnLocation switch
{
SpawnLocationType.MainSub => Item.ItemList.FindAll(it => it.Submarine == Submarine.MainSub),
SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null && it.ParentRuin == null),
SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsOutpost),
SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsWreck),
SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.ParentRuin != null),
_ => throw new NotImplementedException()
};
var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandom();
if (item != null) { return item; }
var target = ParentEvent.GetTargets(SpawnPointTag).GetRandom();
if (target != null) { return target; }
}
SpawnType? spawnPointType = null;
if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; }
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable());
}
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<string> moduleFlags = null, IEnumerable<string> spawnpointTags = null)
{
List<WayPoint> potentialSpawnPoints = spawnLocation switch
{
SpawnLocationType.MainSub => WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine.MainSub && wp.CurrentHull != null),
SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null && wp.ParentRuin == null),
SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost),
SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsWreck),
SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.ParentRuin != null),
_ => throw new NotImplementedException()
};
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed);
if (moduleFlags != null && moduleFlags.Any())
{
List<WayPoint> spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Any(moduleFlags.Contains) ?? false).ToList();
if (spawnPoints.Any())
{
potentialSpawnPoints = spawnPoints;
}
}
if (spawnpointTags != null && spawnpointTags.Any())
{
var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag)))
.Where(wp => wp.ConnectedDoor == null && !wp.isObstructed);
if (spawnPoints.Any())
{
potentialSpawnPoints = spawnPoints.ToList();
}
}
if (potentialSpawnPoints.Count == 0)
{
DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})");
return null;
}
IEnumerable<WayPoint> validSpawnPoints;
if (spawnPointType.HasValue)
{
validSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.SpawnType == spawnPointType.Value);
}
else
{
validSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.SpawnType != SpawnType.Path);
if (!validSpawnPoints.Any()) { validSpawnPoints = potentialSpawnPoints; }
}
//don't spawn in an airlock module if there are other options
var airlockSpawnPoints = validSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Contains("airlock") ?? false);
if (airlockSpawnPoints.Count() < validSpawnPoints.Count())
{
validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints);
}
if (!validSpawnPoints.Any())
{
DebugConsole.ThrowError($"Could not find a spawn point of the correct type for a SpawnAction (spawn location: {spawnLocation}, type: {spawnPointType}, module flags: {((moduleFlags == null || !moduleFlags.Any()) ? "none" : string.Join(", ", moduleFlags))})");
return potentialSpawnPoints.GetRandom();
}
//if not trying to spawn at a tagged spawnpoint, favor spawnpoints without tags
if (spawnpointTags == null || !spawnpointTags.Any())
{
var spawnPoints = validSpawnPoints.Where(wp => !wp.Tags.Any());
if (spawnPoints.Any())
{
validSpawnPoints = spawnPoints.ToList();
}
}
return validSpawnPoints.GetRandom();
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(spawned)} {nameof(SpawnAction)} -> (Spawned entity: {spawnedEntity.ColorizeObject()})";
}
}
}
@@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
partial class StatusEffectAction : EventAction
{
private readonly List<StatusEffect> effects = new List<StatusEffect>();
private int actionIndex;
[Serialize("", true)]
public string TargetTag { get; set; }
public StatusEffectAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
actionIndex = 0;
foreach (XElement subElement in parentEvent.Prefab.ConfigElement.Descendants())
{
if (subElement == element) { break; }
actionIndex++;
}
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
effects.Add(StatusEffect.Load(subElement, $"{nameof(StatusEffectAction)} ({parentEvent.Prefab.Identifier})"));
break;
}
}
}
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
var targets = ParentEvent.GetTargets(TargetTag);
foreach (StatusEffect effect in effects)
{
foreach (var target in targets)
{
effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity);
}
}
#if SERVER
ServerWrite(targets);
#endif
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(StatusEffectAction)} -> (TargetTag: {TargetTag.ColorizeObject()}";
}
}
}
@@ -0,0 +1,102 @@
using Barotrauma.Extensions;
using System;
using System.Xml.Linq;
namespace Barotrauma
{
class TagAction : EventAction
{
[Serialize("", true)]
public string Criteria { get; set; }
[Serialize("", true)]
public string Tag { get; set; }
private bool isFinished = false;
public TagAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
private void TagPlayers()
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer);
}
private void TagBots()
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot);
}
private void TagCrew()
{
#if CLIENT
GameMain.GameSession.CrewManager.GetCharacters().ForEach(c => ParentEvent.AddTarget(Tag, c));
#else
TagPlayers(); TagBots(); //TODO: this seems like it would tag more than it should, fix
#endif
}
private void TagStructuresByIdentifier(string identifier)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && s.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase));
}
private void TagItemsByIdentifier(string identifier)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && it.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase));
}
private void TagItemsByTag(string tag)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && it.HasTag(tag));
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
string[] criteriaSplit = Criteria.Split(';');
foreach (string entry in criteriaSplit)
{
string[] kvp = entry.Split(':');
switch (kvp[0].Trim().ToLowerInvariant())
{
case "player":
TagPlayers();
break;
case "bot":
TagBots();
break;
case "crew":
TagCrew();
break;
case "structureidentifier":
if (kvp.Length > 1) { TagStructuresByIdentifier(kvp[1].Trim()); }
break;
case "itemidentifier":
if (kvp.Length > 1) { TagItemsByIdentifier(kvp[1].Trim()); }
break;
case "itemtag":
if (kvp.Length > 1) { TagItemsByTag(kvp[1].Trim()); }
break;
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TagAction)} -> (Criteria: {Criteria.ColorizeObject()}, Tag: {Tag.ColorizeObject()})";
}
}
}
@@ -0,0 +1,173 @@
using Microsoft.Xna.Framework;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class TriggerAction : EventAction
{
[Serialize("", true, description: "Tag of the first entity that will be used for trigger checks.")]
public string Target1Tag { get; set; }
[Serialize("", true, description: "Tag of the second entity that will be used for trigger checks.")]
public string Target2Tag { get; set; }
[Serialize("", true, description: "If set, the first target has to be within an outpost module of this type.")]
public string TargetModuleType { get; set; }
[Serialize("", true, description: "Tag to apply to the first entity when the trigger check succeeds.")]
public string ApplyToTarget1 { get; set; }
[Serialize("", true, description: "Tag to apply to the second entity when the trigger check succeeds.")]
public string ApplyToTarget2 { get; set; }
[Serialize(0.0f, true, description: "Range both entities must be within to activate the trigger.")]
public float Radius { get; set; }
[Serialize(true, true, description: "If true, characters who are being targeted by some enemy cannot trigger the event.")]
public bool DisableInCombat { get; set; }
private float distance;
public TriggerAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
TargetModuleType = TargetModuleType?.ToLowerInvariant();
}
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
var targets1 = ParentEvent.GetTargets(Target1Tag);
if (!targets1.Any()) { return; }
foreach (Entity e1 in targets1)
{
if (DisableInCombat && IsInCombat(e1)) { continue; }
if (!string.IsNullOrEmpty(TargetModuleType))
{
if (IsCloseEnoughToHull(e1, out Hull hull))
{
Trigger(e1, hull);
return;
}
continue;
}
var targets2 = ParentEvent.GetTargets(Target2Tag);
foreach (Entity e2 in targets2)
{
if (e1 == e2) { continue; }
if (DisableInCombat && IsInCombat(e2)) { continue; }
Vector2 pos1 = e1.WorldPosition;
Vector2 pos2 = e2.WorldPosition;
distance = Vector2.Distance(pos1, pos2);
if (((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) ||
((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) ||
Vector2.DistanceSquared(pos1, pos2) < Radius * Radius)
{
Trigger(e1, e2);
return;
}
}
}
}
private bool IsCloseEnoughToHull(Entity e, out Hull hull)
{
hull = null;
if (Radius <= 0)
{
if (e is Character character && character.CurrentHull != null && character.CurrentHull.OutpostModuleTags.Contains(TargetModuleType))
{
hull = character.CurrentHull;
return true;
}
else if (e is Item item && item.CurrentHull != null && item.CurrentHull.OutpostModuleTags.Contains(TargetModuleType))
{
hull = item.CurrentHull;
return true;
}
return false;
}
else
{
foreach (Hull potentialHull in Hull.hullList)
{
if (!potentialHull.OutpostModuleTags.Contains(TargetModuleType)) { continue; }
Rectangle hullRect = potentialHull.WorldRect;
hullRect.Inflate(Radius, Radius);
if (Submarine.RectContains(hullRect, e.WorldPosition))
{
hull = potentialHull;
return true;
}
}
return false;
}
}
private bool IsInCombat(Entity entity)
{
if (!(entity is Character character)) { return false; }
foreach (Character c in Character.CharacterList)
{
if (c.IsDead || c.Removed || c.IsIncapacitated || !c.Enabled) { continue; }
if (c.IsBot && c.AIController is HumanAIController humanAi)
{
if (humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective &&
combatObjective.Enemy == character)
{
return true;
}
}
else if (c.AIController is EnemyAIController enemyAI && (enemyAI.State == AIState.Aggressive || enemyAI.State == AIState.Attack))
{
if (enemyAI.SelectedAiTarget?.Entity == character || c.CurrentHull == character.CurrentHull)
{
return true;
}
}
}
return false;
}
private void Trigger(Entity entity1, Entity entity2)
{
if (!string.IsNullOrEmpty(ApplyToTarget1))
{
ParentEvent.AddTarget(ApplyToTarget1, entity1);
}
if (!string.IsNullOrEmpty(ApplyToTarget2))
{
ParentEvent.AddTarget(ApplyToTarget2, entity2);
}
isFinished = true;
}
public override string ToDebugString()
{
if (string.IsNullOrEmpty(TargetModuleType))
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {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()})";
}
}
}
}
@@ -0,0 +1,48 @@
using System.Xml.Linq;
namespace Barotrauma
{
class TriggerEventAction : EventAction
{
[Serialize("", true)]
public string Identifier { get; set; }
private bool isFinished;
public TriggerEventAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (GameMain.GameSession?.EventManager != null)
{
var eventPrefab = EventSet.GetEventPrefab(Identifier);
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.");
}
else
{
GameMain.GameSession.EventManager.QueuedEvents.Enqueue(eventPrefab.CreateInstance());
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TriggerEventAction)} -> (EventPrefab: {Identifier.ColorizeObject()})";
}
}
}
@@ -0,0 +1,38 @@
using System;
using System.Xml.Linq;
namespace Barotrauma
{
class WaitAction : EventAction
{
[Serialize(0.0f, true)]
public float Time { get; set; }
private float timeRemaining;
public WaitAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
timeRemaining = Time;
}
public override bool IsFinished(ref string goTo)
{
return timeRemaining <= 0;
}
public override void Reset()
{
timeRemaining = Time;
}
public override void Update(float deltaTime)
{
timeRemaining -= deltaTime;
if (timeRemaining < 0.0f) { timeRemaining = 0.0f; }
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(timeRemaining <= 0)} {nameof(WaitAction)} -> (Remaining: {timeRemaining.ColorizeObject()}, Time: {Time.ColorizeObject()})";
}
}
}
@@ -8,6 +8,13 @@ namespace Barotrauma
{
partial class EventManager
{
public enum NetworkEventType
{
CONVERSATION,
STATUSEFFECT,
MISSION
}
const float IntensityUpdateInterval = 5.0f;
const float CalculateDistanceTraveledInterval = 5.0f;
@@ -42,11 +49,11 @@ namespace Barotrauma
private float roundDuration;
private readonly List<ScriptedEventSet> pendingEventSets = new List<ScriptedEventSet>();
private readonly List<EventSet> pendingEventSets = new List<EventSet>();
private readonly Dictionary<ScriptedEventSet, List<ScriptedEvent>> selectedEvents = new Dictionary<ScriptedEventSet, List<ScriptedEvent>>();
private readonly Dictionary<EventSet, List<Event>> selectedEvents = new Dictionary<EventSet, List<Event>>();
private readonly List<ScriptedEvent> activeEvents = new List<ScriptedEvent>();
private readonly List<Event> activeEvents = new List<Event>();
#if DEBUG && SERVER
private DateTime nextIntensityLogTime;
@@ -61,11 +68,13 @@ namespace Barotrauma
get { return currentIntensity; }
}
public List<ScriptedEvent> ActiveEvents
public List<Event> ActiveEvents
{
get { return activeEvents; }
}
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
public EventManager()
{
isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
@@ -79,20 +88,34 @@ namespace Barotrauma
pendingEventSets.Clear();
selectedEvents.Clear();
activeEvents.Clear();
pathFinder = new PathFinder(WayPoint.WayPointList, indoorsSteering: false);
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(Level.Loaded.StartPosition), ConvertUnits.ToSimUnits(Level.Loaded.EndPosition));
totalPathLength = steeringPath.TotalLength;
totalPathLength = 0.0f;
if (level != null)
{
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(Level.Loaded.StartPosition), ConvertUnits.ToSimUnits(Level.Loaded.EndPosition));
totalPathLength = steeringPath.TotalLength;
}
this.level = level;
SelectSettings();
var initialEventSet = SelectRandomEvents(ScriptedEventSet.List);
var initialEventSet = SelectRandomEvents(EventSet.List);
if (initialEventSet != null)
{
pendingEventSets.Add(initialEventSet);
CreateEvents(initialEventSet);
}
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.RemoveRange(0, level.LevelData.EventHistory.Count - 10);
}
}
PreloadContent(GetFilesToPreload());
@@ -102,7 +125,7 @@ namespace Barotrauma
currentIntensity = targetIntensity;
eventCoolDown = 0.0f;
}
private void SelectSettings()
{
if (EventManagerSettings.List.Count == 0)
@@ -111,6 +134,17 @@ namespace Barotrauma
}
if (level == null)
{
#if CLIENT
if (GameMain.GameSession.GameMode is TestGameMode)
{
settings = EventManagerSettings.List[Rand.Int(EventManagerSettings.List.Count, Rand.RandSync.Server)];
if (settings != null)
{
eventThreshold = settings.DefaultEventThreshold;
}
return;
}
#endif
throw new InvalidOperationException("Could not select EventManager settings (level not set).");
}
@@ -135,11 +169,11 @@ namespace Barotrauma
public IEnumerable<ContentFile> GetFilesToPreload()
{
foreach (List<ScriptedEvent> eventList in selectedEvents.Values)
foreach (List<Event> eventList in selectedEvents.Values)
{
foreach (ScriptedEvent scriptedEvent in eventList)
foreach (Event ev in eventList)
{
foreach (ContentFile contentFile in scriptedEvent.GetFilesToPreload())
foreach (ContentFile contentFile in ev.GetFilesToPreload())
{
yield return contentFile;
}
@@ -265,8 +299,16 @@ namespace Barotrauma
preloadedSprites.Clear();
}
private void CreateEvents(ScriptedEventSet eventSet)
private float CalculateCommonness(Pair<EventPrefab, float> eventPrefab)
{
float retVal = eventPrefab.Second;
if (level.LevelData.EventHistory.Contains(eventPrefab.First)) { retVal *= 0.1f; }
return retVal;
}
private void CreateEvents(EventSet eventSet)
{
if (level == null) { return; }
int applyCount = 1;
if (eventSet.PerRuin)
{
@@ -283,17 +325,22 @@ namespace Barotrauma
if (eventSet.EventPrefabs.Count > 0)
{
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var eventPrefab = ToolBox.SelectWeightedRandom(eventSet.EventPrefabs, eventSet.EventPrefabs.Select(e => e.Commonness).ToList(), rand);
if (eventPrefab != null)
List<Pair<EventPrefab, float>> unusedEvents = new List<Pair<EventPrefab, float>>(eventSet.EventPrefabs);
for (int j = 0; j < eventSet.EventCount; j++)
{
var newEvent = eventPrefab.CreateInstance();
newEvent.Init(true);
DebugConsole.Log("Initialized event " + newEvent.ToString());
if (!selectedEvents.ContainsKey(eventSet))
var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => CalculateCommonness(e)).ToList(), rand);
if (eventPrefab != null)
{
selectedEvents.Add(eventSet, new List<ScriptedEvent>());
var newEvent = eventPrefab.First.CreateInstance();
newEvent.Init(true);
DebugConsole.Log("Initialized event " + newEvent.ToString());
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
}
selectedEvents[eventSet].Add(newEvent);
unusedEvents.Remove(eventPrefab);
}
selectedEvents[eventSet].Add(newEvent);
}
}
if (eventSet.ChildSets.Count > 0)
@@ -304,19 +351,19 @@ namespace Barotrauma
}
else
{
foreach (ScriptedEventPrefab eventPrefab in eventSet.EventPrefabs)
foreach (Pair<EventPrefab, float> eventPrefab in eventSet.EventPrefabs)
{
var newEvent = eventPrefab.CreateInstance();
var newEvent = eventPrefab.First.CreateInstance();
newEvent.Init(true);
DebugConsole.Log("Initialized event " + newEvent.ToString());
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<ScriptedEvent>());
selectedEvents.Add(eventSet, new List<Event>());
}
selectedEvents[eventSet].Add(newEvent);
}
foreach (ScriptedEventSet childEventSet in eventSet.ChildSets)
foreach (EventSet childEventSet in eventSet.ChildSets)
{
CreateEvents(childEventSet);
}
@@ -324,16 +371,22 @@ namespace Barotrauma
}
}
private ScriptedEventSet SelectRandomEvents(List<ScriptedEventSet> eventSets)
private EventSet SelectRandomEvents(List<EventSet> eventSets)
{
if (level == null) { return null; }
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var allowedEventSets =
eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty);
eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && level.LevelData.Type == es.LevelType);
if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.Map?.CurrentLocation?.Type != null)
{
allowedEventSets = allowedEventSets.Where(set => set.LocationTypeIdentifiers == null || set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, campaign.Map.CurrentLocation.Type.Identifier, StringComparison.OrdinalIgnoreCase)));
}
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
float randomNumber = (float)rand.NextDouble() * totalCommonness;
foreach (ScriptedEventSet eventSet in allowedEventSets)
foreach (EventSet eventSet in allowedEventSets)
{
float commonness = eventSet.GetCommonness(level);
if (randomNumber <= commonness)
@@ -346,7 +399,7 @@ namespace Barotrauma
return null;
}
private bool CanStartEventSet(ScriptedEventSet eventSet)
private bool CanStartEventSet(EventSet eventSet)
{
ISpatialEntity refEntity = GetRefEntity();
float distFromStart = Vector2.Distance(refEntity.WorldPosition, level.StartPosition);
@@ -380,7 +433,8 @@ namespace Barotrauma
public void Update(float deltaTime)
{
if (!Enabled) { return; }
if (!Enabled || level == null) { return; }
if (GameMain.GameSession.Campaign?.DisableEvents ?? false) { return; }
//clients only calculate the intensity but don't create any events
//(the intensity is used for controlling the background music)
@@ -421,46 +475,49 @@ namespace Barotrauma
}
eventThreshold += settings.EventThresholdIncrease * deltaTime;
if (eventCoolDown > 0.0f)
{
eventCoolDown -= deltaTime;
}
else if (currentIntensity < eventThreshold)
eventCoolDown -= deltaTime;
if (currentIntensity < eventThreshold)
{
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
{
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))
{
//start events in this set
foreach (ScriptedEvent scriptedEvent in selectedEvents[eventSet])
foreach (Event ev in selectedEvents[eventSet])
{
activeEvents.Add(scriptedEvent);
activeEvents.Add(ev);
}
}
//add child event sets to pending
foreach (ScriptedEventSet childEventSet in eventSet.ChildSets)
foreach (EventSet childEventSet in eventSet.ChildSets)
{
if (selectedEvents.ContainsKey(childEventSet))
{
pendingEventSets.Add(childEventSet);
}
pendingEventSets.Add(childEventSet);
}
}
eventThreshold = settings.DefaultEventThreshold;
eventCoolDown = settings.EventCooldown;
}
foreach (ScriptedEvent ev in activeEvents)
foreach (Event ev in activeEvents)
{
if (!ev.IsFinished) { ev.Update(deltaTime); }
}
if (QueuedEvents.Count > 0)
{
activeEvents.Add(QueuedEvents.Dequeue());
}
}
private void CalculateCurrentIntensity(float deltaTime)
@@ -519,22 +576,23 @@ namespace Barotrauma
// hull status (gaps, flooding, fire) --------------------------------------------------------
float holeCount = 0.0f;
floodingAmount = 0.0f;
int hullCount = 0;
float waterAmount = 0.0f;
float totalHullVolume = 0.0f;
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
hullCount++;
if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; }
if (hull.RoomName != null && hull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; }
foreach (Gap gap in hull.ConnectedGaps)
{
if (!gap.IsRoomToRoom) holeCount += gap.Open;
}
floodingAmount += hull.WaterVolume / hull.Volume;
waterAmount += hull.WaterVolume;
totalHullVolume += hull.Volume;
fireAmount += hull.FireSources.Sum(fs => fs.Size.X);
}
if (hullCount > 0)
if (totalHullVolume > 0)
{
floodingAmount = floodingAmount / hullCount;
floodingAmount = waterAmount / totalHullVolume;
}
//hull integrity at 0.0 if there are 10 or more wide-open holes
@@ -546,7 +604,14 @@ namespace Barotrauma
//flooding less than 10% of the sub is ignored
//to prevent ballast tanks from affecting the intensity
if (floodingAmount < 0.1f) floodingAmount = 0.0f;
if (floodingAmount < 0.1f)
{
floodingAmount = 0.0f;
}
else
{
floodingAmount *= 1.5f;
}
// calculate final intensity --------------------------------------------------------
@@ -558,8 +623,8 @@ namespace Barotrauma
if (targetIntensity > currentIntensity)
{
//50 seconds for intensity to go from 0.0 to 1.0
currentIntensity = MathHelper.Min(currentIntensity + 0.02f * IntensityUpdateInterval, targetIntensity);
//25 seconds for intensity to go from 0.0 to 1.0
currentIntensity = MathHelper.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity);
}
else
{
@@ -570,6 +635,7 @@ namespace Barotrauma
private float CalculateDistanceTraveled()
{
if (level == null) { return 0.0f; }
var refEntity = GetRefEntity();
Vector2 target = ConvertUnits.ToSimUnits(Level.Loaded.EndPosition);
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target);
@@ -585,18 +651,47 @@ namespace Barotrauma
}
/// <summary>
/// Finds all actions in a ScriptedEvent
/// </summary>
private static List<Tuple<int, EventAction>> FindActions(ScriptedEvent scriptedEvent)
{
var list = new List<Tuple<int, EventAction>>();
foreach (EventAction eventAction in scriptedEvent.Actions)
{
list.AddRange(FindActionsRecursive(eventAction));
}
return list;
static List<Tuple<int, EventAction>> FindActionsRecursive(EventAction eventAction, int ident = 1)
{
var eventActions = new List<Tuple<int, EventAction>> { Tuple.Create(ident, eventAction) };
ident++;
foreach (var action in eventAction.GetSubActions())
{
eventActions.AddRange(FindActionsRecursive(action, ident));
}
return eventActions;
}
}
/// <summary>
/// Get the entity that should be used in determining how far the player has progressed in the level.
/// = The submarine or player character that has progressed the furthest.
/// </summary>
private ISpatialEntity GetRefEntity()
public static ISpatialEntity GetRefEntity()
{
ISpatialEntity refEntity = Submarine.MainSub;
#if CLIENT
if (Character.Controlled != null)
{
if (Character.Controlled.Submarine != null &&
Character.Controlled.Submarine.Info.Type == SubmarineInfo.SubmarineType.Player)
Character.Controlled.Submarine.Info.Type == SubmarineType.Player)
{
refEntity = Character.Controlled.Submarine;
}
@@ -613,7 +708,7 @@ namespace Barotrauma
//Otherwise the system could be abused by for example making a respawned player wait
//close to the destination outpost
if (client.Character.Submarine != null &&
client.Character.Submarine.Info.Type == SubmarineInfo.SubmarineType.Player)
client.Character.Submarine.Info.Type == SubmarineType.Player)
{
if (client.Character.Submarine.WorldPosition.X > refEntity.WorldPosition.X)
{
@@ -1,19 +1,19 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
class ScriptedEventPrefab
class EventPrefab
{
public readonly XElement ConfigElement;
public readonly Type EventType;
public readonly string MusicType;
public readonly float SpawnProbability;
public float Commonness;
public string Identifier;
public ScriptedEventPrefab(XElement element)
public EventPrefab(XElement element)
{
ConfigElement = element;
@@ -31,13 +31,15 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\".");
}
Identifier = ConfigElement.GetAttributeString("identifier", string.Empty);
Commonness = element.GetAttributeFloat("commonness", 1.0f);
SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1);
}
public ScriptedEvent CreateInstance()
public Event CreateInstance()
{
ConstructorInfo constructor = EventType.GetConstructor(new[] { typeof(ScriptedEventPrefab) });
ConstructorInfo constructor = EventType.GetConstructor(new[] { typeof(EventPrefab) });
object instance = null;
try
{
@@ -48,7 +50,7 @@ namespace Barotrauma
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
}
return (ScriptedEvent)instance;
return (Event)instance;
}
}
}
@@ -1,39 +1,78 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class ScriptedEventSet
class EventSet
{
internal class EventDebugStats
{
public readonly ScriptedEventSet RootSet;
public readonly EventSet RootSet;
public readonly Dictionary<string, int> MonsterCounts = new Dictionary<string, int>();
public EventDebugStats(ScriptedEventSet rootSet)
public EventDebugStats(EventSet rootSet)
{
RootSet = rootSet;
}
}
public static List<ScriptedEventSet> List
public static List<EventSet> List
{
get;
private set;
}
public static readonly List<EventPrefab> PrefabList = new List<EventPrefab>();
#if CLIENT
private static readonly Dictionary<string, Sprite> EventSprites = new Dictionary<string, Sprite>();
public static Sprite GetEventSprite(string identifier)
{
if (string.IsNullOrWhiteSpace(identifier)) { return null; }
foreach (var (key, value) in EventSprites)
{
if (key.Equals(identifier, StringComparison.OrdinalIgnoreCase)) { return value; }
}
return null;
}
#endif
public static List<EventPrefab> GetAllEventPrefabs()
{
List<EventPrefab> eventPrefabs = new List<EventPrefab>(PrefabList);
foreach (var eventSet in List)
{
eventPrefabs.AddRange(eventSet.EventPrefabs.Select(ep => ep.First));
foreach (var childSet in eventSet.ChildSets)
{
eventPrefabs.AddRange(childSet.EventPrefabs.Select(ep => ep.First));
}
}
return eventPrefabs;
}
public static EventPrefab GetEventPrefab(string identifer)
{
return GetAllEventPrefabs().Find(prefab => string.Equals(prefab.Identifier, identifer, StringComparison.Ordinal));
}
//0-100
public readonly float MinLevelDifficulty, MaxLevelDifficulty;
public readonly LevelData.LevelType LevelType;
public readonly string[] LocationTypeIdentifiers;
public readonly bool ChooseRandom;
public readonly int EventCount = 1;
public readonly float MinDistanceTraveled;
public readonly float MinMissionTime;
@@ -42,14 +81,17 @@ namespace Barotrauma
public readonly bool AllowAtStart;
public readonly bool IgnoreCoolDown;
public readonly bool PerRuin;
public readonly bool PerWreck;
public readonly Dictionary<string, float> Commonness;
public readonly List<ScriptedEventPrefab> EventPrefabs;
//Pair.First: event prefab, Pair.Second: commonness
public readonly List<Pair<EventPrefab, float>> EventPrefabs;
public readonly List<ScriptedEventSet> ChildSets;
public readonly List<EventSet> ChildSets;
public string DebugIdentifier
{
@@ -57,24 +99,39 @@ namespace Barotrauma
private set;
} = "";
private ScriptedEventSet(XElement element, string debugIdentifier)
private EventSet(XElement element, string debugIdentifier, EventSet parentSet = null)
{
DebugIdentifier = element.GetAttributeString("identifier", null) ?? debugIdentifier;
Commonness = new Dictionary<string, float>();
EventPrefabs = new List<ScriptedEventPrefab>();
ChildSets = new List<ScriptedEventSet>();
EventPrefabs = new List<Pair<EventPrefab, float>>();
ChildSets = new List<EventSet>();
MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0);
MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty);
string levelTypeStr = element.GetAttributeString("leveltype", "LocationConnection");
if (!Enum.TryParse(levelTypeStr, true, out LevelType))
{
DebugConsole.ThrowError($"Error in event set \"{debugIdentifier}\". \"{levelTypeStr}\" is not a valid level type.");
}
string[] locationTypeStr = element.GetAttributeStringArray("locationtype", null);
if (locationTypeStr != null)
{
LocationTypeIdentifiers = locationTypeStr;
if (LocationType.List.Any()) { CheckLocationTypeErrors(); }
}
MinIntensity = element.GetAttributeFloat("minintensity", 0.0f);
MaxIntensity = Math.Max(element.GetAttributeFloat("maxintensity", 100.0f), MinIntensity);
ChooseRandom = element.GetAttributeBool("chooserandom", false);
EventCount = element.GetAttributeInt("eventcount", 1);
MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f);
MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f);
AllowAtStart = element.GetAttributeBool("allowatstart", false);
IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? false);
PerRuin = element.GetAttributeBool("perruin", false);
PerWreck = element.GetAttributeBool("perwreck", false);
@@ -98,25 +155,59 @@ namespace Barotrauma
}
break;
case "eventset":
ChildSets.Add(new ScriptedEventSet(subElement, this.DebugIdentifier + "-" + ChildSets.Count));
ChildSets.Add(new EventSet(subElement, this.DebugIdentifier + "-" + ChildSets.Count, this));
break;
default:
EventPrefabs.Add(new ScriptedEventPrefab(subElement));
//an element with just an identifier = reference to an event prefab
if (!subElement.HasElements && subElement.Attributes().First().Name.ToString().Equals("identifier", StringComparison.OrdinalIgnoreCase))
{
string identifier = subElement.GetAttributeString("identifier", "");
var prefab = PrefabList.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase));
if (prefab == null)
{
DebugConsole.ThrowError($"Error in event set \"{debugIdentifier}\" - could not find the event prefab \"{identifier}\".");
}
else
{
float commonness = subElement.GetAttributeFloat("commonness", prefab.Commonness);
EventPrefabs.Add(new Pair<EventPrefab, float>( prefab, commonness));
}
}
else
{
var prefab = new EventPrefab(subElement);
EventPrefabs.Add(new Pair<EventPrefab, float>(prefab, prefab.Commonness));
}
break;
}
}
}
public void CheckLocationTypeErrors()
{
if (LocationTypeIdentifiers == null) { return; }
foreach (string locationTypeId in LocationTypeIdentifiers)
{
if (!LocationType.List.Any(lt => lt.Identifier.Equals(locationTypeId, StringComparison.OrdinalIgnoreCase)))
{
DebugConsole.ThrowError($"Error in event set \"{DebugIdentifier}\". Location type \"{locationTypeId}\" not found.");
}
}
}
public float GetCommonness(Level level)
{
string key = level.GenerationParams?.Name ?? "";
return Commonness.ContainsKey(key) ?
Commonness[key] : Commonness[""];
string key = level.GenerationParams?.Identifier ?? "";
return Commonness.ContainsKey(key) ? Commonness[key] : Commonness[""];
}
public static void LoadPrefabs()
{
List = new List<ScriptedEventSet>();
#if CLIENT
EventSprites.ForEach(pair => pair.Value?.Remove());
EventSprites.Clear();
#endif
List = new List<EventSet>();
var configFiles = GameMain.Instance.GetFilesOfType(ContentType.RandomEvents);
if (!configFiles.Any())
@@ -125,6 +216,9 @@ namespace Barotrauma
return;
}
List<XElement> configElements = new List<XElement>();
Dictionary<XElement, string> filePaths = new Dictionary<XElement, string>();
foreach (ContentFile configFile in configFiles)
{
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
@@ -137,12 +231,61 @@ namespace Barotrauma
List.Clear();
}
int i = 0;
foreach (XElement element in doc.Root.Elements())
{
if (!element.Name.ToString().Equals("eventset", StringComparison.OrdinalIgnoreCase)) { continue; }
List.Add(new ScriptedEventSet(element, i.ToString()));
i++;
configElements.Add(element);
filePaths[element] = configFile.Path;
}
}
//load event prefabs first so we can link to them when loading event sets
foreach (XElement element in configElements)
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "eventprefabs":
foreach (var subElement in element.Elements())
{
// Warn if an event prefab has no identifier as this would make it impossible to refer to
if (!element.GetAttributeBool("suppresswarnings", false) && string.IsNullOrWhiteSpace(subElement.GetAttributeString("identifier", string.Empty)))
{
DebugConsole.AddWarning($"An event prefab {subElement.Name} in {filePaths[element]} is missing an identifier.");
}
PrefabList.Add(new EventPrefab(subElement));
}
break;
case "eventsprites":
#if CLIENT
foreach (var subElement in element.Elements())
{
string identifier = subElement.GetAttributeString("identifier", string.Empty);
if (EventSprites.ContainsKey(identifier))
{
EventSprites[identifier]?.Remove();
EventSprites[identifier] = new Sprite(subElement);
continue;
}
else
{
EventSprites.Add(identifier, new Sprite(subElement));
}
}
#endif
break;
}
}
int i = 0;
foreach (XElement element in configElements)
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "eventset":
List.Add(new EventSet(element, i.ToString()));
i++;
break;
}
}
}
@@ -170,7 +313,7 @@ namespace Barotrauma
List<EventDebugStats> stats = new List<EventDebugStats>();
for (int i = 0; i < simulatedRoundCount; i++)
{
ScriptedEventSet selectedSet = List.Where(s => difficulty >= s.MinLevelDifficulty && difficulty <= s.MaxLevelDifficulty).GetRandom();
EventSet selectedSet = List.Where(s => difficulty >= s.MinLevelDifficulty && difficulty <= s.MaxLevelDifficulty).GetRandom();
if (selectedSet == null) { continue; }
var newStats = new EventDebugStats(selectedSet);
CheckEventSet(newStats, selectedSet);
@@ -181,21 +324,26 @@ namespace Barotrauma
return debugLines;
static void CheckEventSet(EventDebugStats stats, ScriptedEventSet thisSet)
static void CheckEventSet(EventDebugStats stats, EventSet thisSet)
{
if (thisSet.ChooseRandom)
{
var eventPrefab = ToolBox.SelectWeightedRandom(thisSet.EventPrefabs, thisSet.EventPrefabs.Select(e => e.Commonness).ToList(), Rand.RandSync.Unsynced);
if (eventPrefab != null)
List<Pair<EventPrefab, float>> unusedEvents = new List<Pair<EventPrefab, float>>(thisSet.EventPrefabs);
for (int i = 0; i < thisSet.EventCount; i++)
{
AddEvent(stats, eventPrefab);
var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Second).ToList(), Rand.RandSync.Unsynced);
if (eventPrefab != null)
{
AddEvent(stats, eventPrefab.First);
unusedEvents.Remove(eventPrefab);
}
}
}
else
{
foreach (var eventPrefab in thisSet.EventPrefabs)
{
AddEvent(stats, eventPrefab);
AddEvent(stats, eventPrefab.First);
}
}
foreach (var childSet in thisSet.ChildSets)
@@ -204,7 +352,7 @@ namespace Barotrauma
}
}
static void AddEvent(EventDebugStats stats, ScriptedEventPrefab eventPrefab)
static void AddEvent(EventDebugStats stats, EventPrefab eventPrefab)
{
if (eventPrefab.EventType == typeof(MonsterEvent))
{
@@ -6,7 +6,7 @@ using Microsoft.Xna.Framework;
namespace Barotrauma
{
class MalfunctionEvent : ScriptedEvent
class MalfunctionEvent : Event
{
private string[] targetItemIdentifiers;
@@ -25,7 +25,7 @@ namespace Barotrauma
return "MalfunctionEvent (" + string.Join(", ", targetItemIdentifiers) + ")";
}
public MalfunctionEvent(ScriptedEventPrefab prefab)
public MalfunctionEvent(EventPrefab prefab)
: base(prefab)
{
targetItems = new List<Item>();
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -11,8 +12,8 @@ namespace Barotrauma
private readonly XElement itemConfig;
private readonly List<Item> items = new List<Item>();
private readonly Dictionary<Item, UInt16> itemIDs = new Dictionary<Item, UInt16>();
private readonly Dictionary<Item, UInt16> parentInventoryIDs = new Dictionary<Item, UInt16>();
private readonly Dictionary<Item, byte> parentItemContainerIndices = new Dictionary<Item, byte>();
private int requiredDeliveryAmount;
@@ -26,8 +27,8 @@ namespace Barotrauma
private void InitItems()
{
items.Clear();
itemIDs.Clear();
parentInventoryIDs.Clear();
parentItemContainerIndices.Clear();
if (itemConfig == null)
{
@@ -96,12 +97,12 @@ namespace Barotrauma
var item = new Item(itemPrefab, position, cargoRoom.Submarine);
item.FindHull();
items.Add(item);
itemIDs.Add(item, item.ID);
if (parent != null)
if (parent != null && parent.GetComponent<ItemContainer>() != null)
{
parentInventoryIDs.Add(item, parent.ID);
parent.Combine(item, user: null);
parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(parent.GetComponent<ItemContainer>()));
parent.Combine(item, user: null);
}
foreach (XElement subElement in element.Elements())
@@ -116,6 +117,9 @@ namespace Barotrauma
public override void Start(Level level)
{
items.Clear();
parentInventoryIDs.Clear();
if (!IsClient)
{
InitItems();
@@ -105,6 +105,7 @@ namespace Barotrauma
subs = new Submarine[] { Submarine.MainSubs[0], Submarine.MainSubs[1] };
subs[0].TeamID = Character.TeamType.Team1; subs[1].TeamID = Character.TeamType.Team2;
subs[0].NeutralizeBallast(); subs[1].NeutralizeBallast();
subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition));
subs[1].FlipX();
@@ -1,5 +1,6 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -63,6 +64,11 @@ namespace Barotrauma
get { return Prefab.Reward; }
}
public Dictionary<string, float> ReputationRewards
{
get { return Prefab.ReputationRewards; }
}
public bool Completed
{
get { return completed; }
@@ -197,8 +203,22 @@ namespace Barotrauma
public void GiveReward()
{
if (!(GameMain.GameSession.GameMode is CampaignMode mode)) { return; }
mode.Money += Reward;
if (!(GameMain.GameSession.GameMode is CampaignMode campaign)) { return; }
campaign.Money += Reward;
foreach (KeyValuePair<string, float> reputationReward in ReputationRewards)
{
if (reputationReward.Key.Equals("location", StringComparison.OrdinalIgnoreCase))
{
Locations[0].Reputation.Value += reputationReward.Value;
Locations[1].Reputation.Value += reputationReward.Value;
}
else
{
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier.Equals(reputationReward.Key, StringComparison.OrdinalIgnoreCase));
if (faction != null) { faction.Reputation.Value += reputationReward.Value; }
}
}
}
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
@@ -36,9 +37,14 @@ namespace Barotrauma
public readonly bool MultiplayerOnly, SingleplayerOnly;
public readonly string Identifier;
public readonly string TextIdentifier;
private readonly string[] tags;
public IEnumerable<string> Tags
{
get { return tags; }
}
public readonly string Name;
public readonly string Description;
public readonly string SuccessMessage;
@@ -48,6 +54,8 @@ namespace Barotrauma
public readonly string AchievementIdentifier;
public readonly Dictionary<string, float> ReputationRewards = new Dictionary<string, float>();
public readonly int Commonness;
public readonly int Reward;
@@ -107,6 +115,8 @@ namespace Barotrauma
Identifier = element.GetAttributeString("identifier", "");
TextIdentifier = element.GetAttributeString("textidentifier", null) ?? Identifier;
tags = element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true);
Name = TextManager.Get("MissionName." + TextIdentifier, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("MissionDescription." + TextIdentifier, true) ?? element.GetAttributeString("description", "");
Reward = element.GetAttributeInt("reward", 1);
@@ -150,6 +160,24 @@ namespace Barotrauma
subElement.GetAttributeString("from", ""),
subElement.GetAttributeString("to", "")));
break;
case "reputation":
case "reputationreward":
string factionIdentifier = subElement.GetAttributeString("identifier", "");
float amount = subElement.GetAttributeFloat("amount", 0.0f);
if (ReputationRewards.ContainsKey(factionIdentifier))
{
DebugConsole.ThrowError($"Error in mission prefab \"{Identifier}\". Multiple reputation changes defined for the identifier \"{factionIdentifier}\".");
continue;
}
ReputationRewards.Add(factionIdentifier, amount);
if (!factionIdentifier.Equals("location", StringComparison.OrdinalIgnoreCase))
{
if (FactionPrefab.Prefabs != null && !FactionPrefab.Prefabs.Any(p => p.Identifier.Equals(factionIdentifier, StringComparison.OrdinalIgnoreCase)))
{
DebugConsole.ThrowError($"Error in mission prefab \"{Identifier}\". Could not find a faction with the identifier \"{factionIdentifier}\".");
}
}
break;
}
}
@@ -7,11 +7,8 @@ namespace Barotrauma
{
partial class MonsterMission : Mission
{
private readonly string monsterFile;
private readonly int monsterCount;
//string = filename, point = min,max
private readonly HashSet<Tuple<string, Point>> monsterFiles = new HashSet<Tuple<string, Point>>();
private readonly HashSet<Tuple<CharacterPrefab, Point>> monsterPrefabs = new HashSet<Tuple<CharacterPrefab, Point>>();
private readonly List<Character> monsters = new List<Character>();
private readonly List<Vector2> sonarPositions = new List<Vector2>();
@@ -38,28 +35,26 @@ namespace Barotrauma
public MonsterMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
{
monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", null);
if (!string.IsNullOrEmpty(monsterFile))
string speciesName = prefab.ConfigElement.GetAttributeString("monsterfile", null);
if (!string.IsNullOrEmpty(speciesName))
{
var characterPrefab = CharacterPrefab.FindByFilePath(monsterFile);
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab != null)
{
monsterFile = characterPrefab.Identifier;
int monsterCount = Math.Min(prefab.ConfigElement.GetAttributeInt("monstercount", 1), 255);
monsterPrefabs.Add(new Tuple<CharacterPrefab, Point>(characterPrefab, new Point(monsterCount)));
}
else
{
DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".");
}
}
maxSonarMarkerDistance = prefab.ConfigElement.GetAttributeFloat("maxsonarmarkerdistance", 10000.0f);
monsterCount = Math.Min(prefab.ConfigElement.GetAttributeInt("monstercount", 1), 255);
string monsterFileName = monsterFile;
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
string monster = monsterElement.GetAttributeString("character", string.Empty);
if (monsterFileName == null)
{
monsterFileName = monster;
}
speciesName = monsterElement.GetAttributeString("character", string.Empty);
int defaultCount = monsterElement.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
@@ -67,10 +62,24 @@ namespace Barotrauma
}
int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255);
int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255);
monsterFiles.Add(new Tuple<string, Point>(monster, new Point(min, max)));
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab != null)
{
monsterPrefabs.Add(new Tuple<CharacterPrefab, Point>(characterPrefab, new Point(min, max)));
}
else
{
DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".");
}
}
if (monsterPrefabs.Any())
{
var characterParams = new CharacterParams(monsterPrefabs.First().Item1.FilePath);
description = description.Replace("[monster]",
TextManager.Get("character." + characterParams.SpeciesTranslationOverride, returnNull: true) ??
TextManager.Get("character." + characterParams.SpeciesName));
}
description = description.Replace("[monster]",
TextManager.Get("character." + Barotrauma.IO.Path.GetFileNameWithoutExtension(monsterFileName)));
}
public override void Start(Level level)
@@ -88,19 +97,12 @@ namespace Barotrauma
if (!IsClient)
{
Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos);
if (!string.IsNullOrEmpty(monsterFile))
{
for (int i = 0; i < monsterCount; i++)
{
monsters.Add(Character.Create(monsterFile, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false));
}
}
foreach (var monster in monsterFiles)
foreach (var monster in monsterPrefabs)
{
int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1);
for (int i = 0; i < amount; i++)
{
monsters.Add(Character.Create(monster.Item1, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false));
monsters.Add(Character.Create(monster.Item1.Identifier, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false));
}
}
@@ -104,9 +104,9 @@ namespace Barotrauma
public override void Start(Level level)
{
#if SERVER
originalItemID = Entity.NullEntityID;
originalInventoryID = Entity.NullEntityID;
#endif
item = null;
if (!IsClient)
{
//ruin/wreck items are allowed to spawn close to the sub
@@ -129,7 +129,7 @@ namespace Barotrauma
case Level.PositionType.Wreck:
foreach (Item it in suitableItems)
{
if (it.Submarine == null || it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Wreck) { continue; }
if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
Rectangle worldBorders = it.Submarine.Borders;
worldBorders.Location += it.Submarine.WorldPosition.ToPoint();
if (Submarine.RectContains(worldBorders, it.WorldPosition))
@@ -151,9 +151,6 @@ namespace Barotrauma
item.body.FarseerBody.BodyType = BodyType.Kinematic;
item.FindHull();
}
#if SERVER
originalItemID = item.ID;
#endif
for (int i = 0; i < statusEffects.Count; i++)
{
@@ -173,6 +170,7 @@ namespace Barotrauma
foreach (Item it in Item.ItemList)
{
if (!it.HasTag(containerTag)) { continue; }
if (it.NonInteractable) { continue; }
switch (spawnPositionType)
{
case Level.PositionType.Cave:
@@ -183,7 +181,7 @@ namespace Barotrauma
if (it.ParentRuin == null) { continue; }
break;
case Level.PositionType.Wreck:
if (it.Submarine == null || it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Wreck) { continue; }
if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
break;
}
var itemContainer = it.GetComponent<Items.Components.ItemContainer>();
@@ -192,6 +190,7 @@ namespace Barotrauma
{
#if SERVER
originalInventoryID = it.ID;
originalItemContainerIndex = (byte)it.GetComponentIndex(itemContainer);
#endif
break;
} // Placement successful
@@ -221,11 +220,15 @@ namespace Barotrauma
if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
if (showMessageWhenPickedUp)
{
if (!(item.ParentInventory?.Owner is Character)) { return; }
if (!(item.GetRootInventoryOwner() is Character)) { return; }
}
else
{
if (item.CurrentHull?.Submarine == null || item.CurrentHull.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { return; }
Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine;
if (parentSub == null || parentSub.Info.Type != SubmarineType.Player)
{
return;
}
}
State = 1;
break;
@@ -7,7 +7,7 @@ using Barotrauma.Items.Components;
namespace Barotrauma
{
class MonsterEvent : ScriptedEvent
class MonsterEvent : Event
{
private readonly string speciesName;
private readonly int minAmount, maxAmount;
@@ -26,6 +26,12 @@ namespace Barotrauma
private bool spawnPending;
public List<Character> Monsters => monsters;
public Vector2? SpawnPos => spawnPos;
public bool SpawnPending => spawnPending;
public int MinAmount => minAmount;
public int MaxAmount => maxAmount;
public override Vector2 DebugDrawPos
{
get { return spawnPos ?? Vector2.Zero; }
@@ -47,7 +53,7 @@ namespace Barotrauma
}
}
public MonsterEvent(ScriptedEventPrefab prefab)
public MonsterEvent(EventPrefab prefab)
: base (prefab)
{
speciesName = prefab.ConfigElement.GetAttributeString("characterfile", "");
@@ -93,6 +99,11 @@ namespace Barotrauma
}
}
private Submarine GetReferenceSub()
{
return EventManager.GetRefEntity() as Submarine ?? Submarine.MainSub;
}
public override IEnumerable<ContentFile> GetFilesToPreload()
{
string path = CharacterPrefab.FindBySpeciesName(speciesName)?.FilePath;
@@ -110,7 +121,7 @@ namespace Barotrauma
public override bool CanAffectSubImmediately(Level level)
{
float maxRange = Sonar.DefaultSonarRange * 0.8f;
return GetAvailableSpawnPositions().Any(p => Vector2.DistanceSquared(p.Position.ToVector2(), Submarine.MainSub.WorldPosition) < maxRange * maxRange);
return GetAvailableSpawnPositions().Any(p => Vector2.DistanceSquared(p.Position.ToVector2(), GetReferenceSub().WorldPosition) < maxRange * maxRange);
}
public override void Init(bool affectSubImmediately)
@@ -191,10 +202,10 @@ namespace Barotrauma
foreach (var position in availablePositions)
{
Vector2 pos = position.Position.ToVector2();
float dist = Vector2.DistanceSquared(pos, Submarine.MainSub.WorldPosition);
float dist = Vector2.DistanceSquared(pos, GetReferenceSub().WorldPosition);
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (sub.Info.Type != SubmarineType.Player) { continue; }
float minDistToSub = GetMinDistanceToSub(sub);
if (dist > minDistToSub * minDistToSub && dist < closestDist)
{
@@ -209,7 +220,7 @@ namespace Barotrauma
{
foreach (var position in availablePositions)
{
float dist = Vector2.DistanceSquared(position.Position.ToVector2(), Submarine.MainSub.WorldPosition);
float dist = Vector2.DistanceSquared(position.Position.ToVector2(), GetReferenceSub().WorldPosition);
if (dist < closestDist)
{
closestDist = dist;
@@ -223,7 +234,7 @@ namespace Barotrauma
if (!isSubOrWreck)
{
float minDistance = 20000;
availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
availablePositions.RemoveAll(p => Vector2.DistanceSquared(GetReferenceSub().WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
}
if (availablePositions.None())
{
@@ -256,6 +267,11 @@ namespace Barotrauma
int currentIndex = waypoints.IndexOf(nearestWaypoint);
var nextWaypoint = waypoints[Math.Min(currentIndex + 20, waypoints.Count - 1)];
dir = Vector2.Normalize(nextWaypoint.WorldPosition - nearestWaypoint.WorldPosition);
// Ensure that the spawn position is not offset to the left.
if (dir.X < 0)
{
dir.X = 0;
}
}
else
{
@@ -301,7 +317,7 @@ namespace Barotrauma
{
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (submarine.Info.Type != SubmarineType.Player) { continue; }
float minDist = GetMinDistanceToSub(submarine);
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; }
}
@@ -315,7 +331,7 @@ namespace Barotrauma
float minDist = Sonar.DefaultSonarRange * 0.8f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist)
{
someoneNearby = true;
@@ -1,85 +1,203 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class ScriptedEvent
{
protected bool isFinished;
class ScriptedEvent : Event
{
private readonly Dictionary<string, List<Predicate<Entity>>> targetPredicates = new Dictionary<string, List<Predicate<Entity>>>();
private readonly Dictionary<string, List<Entity>> cachedTargets = new Dictionary<string, List<Entity>>();
private int prevEntityCount;
private int prevPlayerCount, prevBotCount;
public int CurrentActionIndex { get; private set; }
public List<EventAction> Actions { get; } = new List<EventAction>();
public Dictionary<string, List<Entity>> Targets { get; } = new Dictionary<string, List<Entity>>();
protected readonly ScriptedEventPrefab prefab;
public bool IsFinished
{
get { return isFinished; }
}
public override string ToString()
{
return "ScriptedEvent (" + prefab.EventType.ToString() +")";
}
public virtual Vector2 DebugDrawPos
{
get
{
return Vector2.Zero;
}
}
public ScriptedEvent(ScriptedEventPrefab prefab)
public ScriptedEvent(EventPrefab prefab) : base(prefab)
{
this.prefab = prefab;
}
public virtual IEnumerable<ContentFile> GetFilesToPreload()
{
yield break;
}
public virtual void Init(bool affectSubImmediately)
{
}
public virtual void Update(float deltaTime)
{
}
public virtual void Finished()
{
isFinished = true;
}
public virtual bool CanAffectSubImmediately(Level level)
{
return true;
}
/*public static List<ScriptedEvent> GenerateInitialEvents(Random random, Level level)
{
if (ScriptedEventPrefab.List == null)
foreach (XElement element in prefab.ConfigElement.Elements())
{
ScriptedEventPrefab.LoadPrefabs();
}
List<ScriptedEvent> events = new List<ScriptedEvent>();
foreach (ScriptedEventPrefab scriptedEvent in ScriptedEventPrefab.List)
{
int minCount = scriptedEvent.MinEventCount.ContainsKey(level.GenerationParams.Name) ?
scriptedEvent.MinEventCount[level.GenerationParams.Name] : scriptedEvent.MinEventCount[""];
int maxCount = scriptedEvent.MaxEventCount.ContainsKey(level.GenerationParams.Name) ?
scriptedEvent.MaxEventCount[level.GenerationParams.Name] : scriptedEvent.MaxEventCount[""];
minCount = Math.Min(minCount, maxCount);
int count = random.Next(maxCount - minCount) + minCount;
for (int i = 0; i < count; i++)
if (element.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
{
ScriptedEvent eventInstance = scriptedEvent.CreateInstance();
events.Add(eventInstance);
DebugConsole.ThrowError($"Error in event prefab \"{prefab.Identifier}\". Status effect configured as an action. Please configure status effects as child elements of a StatusEffectAction.");
continue;
}
var action = EventAction.Instantiate(this, element);
if (action != null) { Actions.Add(action); }
}
if (!Actions.Any())
{
DebugConsole.ThrowError($"Scripted event \"{prefab.Identifier}\" has no actions. The event will do nothing.");
}
}
public void AddTarget(string tag, Entity target)
{
if (target == null)
{
throw new System.ArgumentException("Target was null");
}
if (target.Removed)
{
throw new System.ArgumentException("Target has been removed");
}
if (!Targets.ContainsKey(tag))
{
Targets.Add(tag, new List<Entity>());
}
Targets[tag].Add(target);
if (cachedTargets.ContainsKey(tag))
{
cachedTargets[tag].Add(target);
}
else
{
cachedTargets.Add(tag, new List<Entity> { target });
}
}
public void AddTargetPredicate(string tag, Predicate<Entity> predicate)
{
if (!targetPredicates.ContainsKey(tag))
{
targetPredicates.Add(tag, new List<Predicate<Entity>>());
}
targetPredicates[tag].Add(predicate);
// force re-search for this tag
if (cachedTargets.ContainsKey(tag))
{
cachedTargets.Remove(tag);
}
}
public IEnumerable<Entity> GetTargets(string tag)
{
if (cachedTargets.ContainsKey(tag))
{
if (cachedTargets[tag].Any(t => t.Removed))
{
cachedTargets.Clear();
}
else
{
return cachedTargets[tag];
}
}
return events;
}*/
List<Entity> targetsToReturn = new List<Entity>();
if (Targets.ContainsKey(tag))
{
foreach (Entity e in Targets[tag])
{
if (e.Removed) { continue; }
targetsToReturn.Add(e);
}
}
if (targetPredicates.ContainsKey(tag))
{
foreach (Entity entity in Entity.GetEntities())
{
if (targetPredicates[tag].Any(p => p(entity)))
{
targetsToReturn.Add(entity);
}
}
}
foreach (WayPoint wayPoint in WayPoint.WayPointList)
{
if (wayPoint.Tags.Contains(tag)) { targetsToReturn.Add(wayPoint); }
}
if (Level.Loaded?.StartOutpost != null &&
Level.Loaded.StartOutpost.Info.OutpostNPCs.TryGetValue(tag, out List<Character> outpostNPCs))
{
foreach (Character npc in outpostNPCs)
{
if (npc.Removed) { continue; }
targetsToReturn.Add(npc);
}
}
cachedTargets.Add(tag, targetsToReturn);
return targetsToReturn;
}
public override void Update(float deltaTime)
{
int botCount = 0;
int playerCount = 0;
foreach (Character c in Character.CharacterList)
{
if (c.IsPlayer)
{
playerCount++;
}
else if (c.IsBot)
{
botCount++;
}
}
if (Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount)
{
cachedTargets.Clear();
prevEntityCount = Entity.EntityCount;
prevBotCount = botCount;
prevPlayerCount = playerCount;
}
if (!Actions.Any())
{
Finished();
return;
}
var currentAction = Actions[CurrentActionIndex];
if (!currentAction.CanBeFinished())
{
Finished();
return;
}
string goTo = null;
if (currentAction.IsFinished(ref goTo))
{
if (string.IsNullOrEmpty(goTo))
{
CurrentActionIndex++;
}
else
{
CurrentActionIndex = -1;
Actions.ForEach(a => a.Reset());
for (int i = 0; i < Actions.Count; i++)
{
if (Actions[i].SetGoToTarget(goTo))
{
CurrentActionIndex = i;
break;
}
}
}
if (CurrentActionIndex >= Actions.Count || CurrentActionIndex < 0)
{
Finished();
}
}
else
{
currentAction.Update(deltaTime);
}
}
}
}
@@ -12,22 +12,19 @@ namespace Barotrauma
public static bool OutputDebugInfo = false;
public static void PlaceIfNeeded(GameMode gameMode)
public static void PlaceIfNeeded()
{
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
CampaignMode campaign = gameMode as CampaignMode;
if (campaign == null || !campaign.InitialSuppliesSpawned)
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
if (Submarine.MainSubs[i] == null) { continue; }
List<Submarine> subs = new List<Submarine>() { Submarine.MainSubs[i] };
subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost));
Place(subs);
}
if (campaign != null) { campaign.InitialSuppliesSpawned = true; }
if (Submarine.MainSubs[i] == null || Submarine.MainSubs[i].Info.InitialSuppliesSpawned) { continue; }
List<Submarine> subs = new List<Submarine>() { Submarine.MainSubs[i] };
subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost));
Place(subs);
subs.ForEach(s => s.Info.InitialSuppliesSpawned = true);
}
foreach (var wreck in Submarine.Loaded)
{
if (wreck.Info.IsWreck)
@@ -35,6 +32,12 @@ namespace Barotrauma
Place(wreck.ToEnumerable());
}
}
if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(Level.Loaded.StartOutpost.Info.Name));
Place(Level.Loaded.StartOutpost.ToEnumerable());
}
}
private static void Place(IEnumerable<Submarine> subs)
@@ -54,9 +57,10 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
if (!subs.Contains(item.Submarine)) { continue; }
if (item.GetRootInventoryOwner() is Character) { continue; }
containers.AddRange(item.GetComponents<ItemContainer>());
}
containers.Shuffle();
containers.Shuffle(Rand.RandSync.Server);
foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
{
@@ -74,7 +78,7 @@ namespace Barotrauma
spawnedItems.Clear();
var validContainers = new Dictionary<ItemContainer, PreferredContainer>();
prefabsWithContainer.Shuffle();
prefabsWithContainer.Shuffle(Rand.RandSync.Server);
// Spawn items that have an ItemContainer component first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc)
for (int i = 0; i < prefabsWithContainer.Count; i++)
{
@@ -90,7 +94,7 @@ namespace Barotrauma
// Another pass for items with containers because also they can spawn inside other items (like smg magazine)
prefabsWithContainer.ForEach(i => SpawnItems(i));
// Spawn items that don't have containers last
prefabsWithoutContainer.Shuffle();
prefabsWithoutContainer.Shuffle(Rand.RandSync.Server);
prefabsWithoutContainer.ForEach(i => SpawnItems(i));
if (OutputDebugInfo)
@@ -103,6 +107,30 @@ namespace Barotrauma
}
}
if (GameMain.GameSession?.Level != null &&
GameMain.GameSession.Level.Type == LevelData.LevelType.Outpost &&
GameMain.GameSession.StartLocation?.TakenItems != null)
{
foreach (Location.TakenItem takenItem in GameMain.GameSession.StartLocation.TakenItems)
{
var matchingItem = spawnedItems.Find(it => takenItem.Matches(it));
if (matchingItem == null) { continue; }
var containedItems = spawnedItems.FindAll(it => it.ParentInventory?.Owner == matchingItem);
matchingItem.Remove();
spawnedItems.Remove(matchingItem);
foreach (Item containedItem in containedItems)
{
containedItem.Remove();
spawnedItems.Remove(containedItem);
}
}
}
#if SERVER
foreach (Item spawnedItem in spawnedItems)
{
Entity.Spawner.CreateNetworkEvent(spawnedItem, remove: false);
}
#endif
bool SpawnItems(ItemPrefab itemPrefab)
{
if (itemPrefab == null)
@@ -158,13 +186,13 @@ namespace Barotrauma
private static bool SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer)
{
bool success = false;
if (Rand.Value() > validContainer.Value.SpawnProbability) { return false; }
if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability) { return false; }
// Don't add dangerously reactive materials in thalamus wrecks
if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater"))
{
return false;
}
int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1);
int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.Server);
for (int i = 0; i < amount; i++)
{
if (validContainer.Key.Inventory.IsFull())
@@ -172,17 +200,18 @@ namespace Barotrauma
containers.Remove(validContainer.Key);
break;
}
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine);
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine)
{
SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost,
OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex,
OriginalContainerID = validContainer.Key.Item.OriginalID
};
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = validContainer.Key.Item.Submarine.TeamID;
}
spawnedItems.Add(item);
#if SERVER
Entity.Spawner.CreateNetworkEvent(item, remove: false);
#endif
validContainer.Key.Inventory.TryPutItem(item, null);
validContainer.Key.Inventory.TryPutItem(item, null, createNetworkEvent: false);
containers.AddRange(item.GetComponents<ItemContainer>());
success = true;
}
@@ -1,93 +1,151 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
#if SERVER
using Barotrauma.Networking;
#endif
namespace Barotrauma
{
class PurchasedItem
{
public readonly ItemPrefab ItemPrefab;
public int Quantity;
public ItemPrefab ItemPrefab { get; }
public int Quantity { get; set; }
public PurchasedItem(ItemPrefab itemPrefab, int quantity)
{
this.ItemPrefab = itemPrefab;
this.Quantity = quantity;
ItemPrefab = itemPrefab;
Quantity = quantity;
}
}
class CargoManager
class SoldItem
{
public ItemPrefab ItemPrefab { get; }
public ushort ID { get; }
public bool Removed { get; set; }
public byte SellerID { get; }
public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId)
{
ItemPrefab = itemPrefab;
ID = id;
Removed = removed;
SellerID = sellerId;
}
}
partial class CargoManager
{
public const int MaxQuantity = 100;
private readonly List<PurchasedItem> purchasedItems;
public List<PurchasedItem> ItemsInBuyCrate { get; } = new List<PurchasedItem>();
public List<PurchasedItem> ItemsInSellCrate { get; } = new List<PurchasedItem>();
public List<PurchasedItem> PurchasedItems { get; } = new List<PurchasedItem>();
public List<SoldItem> SoldItems { get; } = new List<SoldItem>();
private readonly CampaignMode campaign;
public Action OnItemsChanged;
private Location location => campaign.Map.CurrentLocation;
public List<PurchasedItem> PurchasedItems
{
get { return purchasedItems; }
}
public Action OnItemsInBuyCrateChanged;
public Action OnItemsInSellCrateChanged;
public Action OnPurchasedItemsChanged;
public Action OnSoldItemsChanged;
public CargoManager(CampaignMode campaign)
{
purchasedItems = new List<PurchasedItem>();
this.campaign = campaign;
}
public void ClearItemsInBuyCrate()
{
ItemsInBuyCrate.Clear();
OnItemsInBuyCrateChanged?.Invoke();
}
public void ClearItemsInSellCrate()
{
ItemsInSellCrate.Clear();
OnItemsInSellCrateChanged?.Invoke();
}
public void SetPurchasedItems(List<PurchasedItem> items)
{
purchasedItems.Clear();
purchasedItems.AddRange(items);
OnItemsChanged?.Invoke();
PurchasedItems.Clear();
PurchasedItems.AddRange(items);
OnPurchasedItemsChanged?.Invoke();
}
public void PurchaseItem(ItemPrefab item, int quantity = 1)
public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity)
{
PurchasedItem purchasedItem = PurchasedItems.Find(pi => pi.ItemPrefab == item);
campaign.Money -= item.GetPrice(campaign.Map.CurrentLocation).BuyPrice * quantity;
if (purchasedItem != null)
PurchasedItem itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab);
if (itemInCrate != null)
{
purchasedItem.Quantity += quantity;
itemInCrate.Quantity += changeInQuantity;
if (itemInCrate.Quantity < 1)
{
ItemsInBuyCrate.Remove(itemInCrate);
}
}
else
else if(changeInQuantity > 0)
{
purchasedItem = new PurchasedItem(item, quantity);
purchasedItems.Add(purchasedItem);
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity);
ItemsInBuyCrate.Add(itemInCrate);
}
OnItemsChanged?.Invoke();
OnItemsInBuyCrateChanged?.Invoke();
}
public void SellItem(PurchasedItem purchasedItem, int quantity = 1)
public void PurchaseItems(List<PurchasedItem> itemsToPurchase, bool removeFromCrate)
{
quantity = Math.Min(purchasedItem.Quantity, quantity);
campaign.Money += purchasedItem.ItemPrefab.GetPrice(campaign.Map.CurrentLocation).BuyPrice * quantity;
purchasedItem.Quantity -= quantity;
if (purchasedItem != null && purchasedItem.Quantity <= 0)
foreach (PurchasedItem item in itemsToPurchase)
{
PurchasedItems.Remove(purchasedItem);
// Add to the purchased items
var purchasedItem = PurchasedItems.Find(pi => pi.ItemPrefab == item.ItemPrefab);
if (purchasedItem != null)
{
purchasedItem.Quantity += item.Quantity;
}
else
{
purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity);
PurchasedItems.Add(purchasedItem);
}
// Exchange money
var itemValue = GetBuyValueAtCurrentLocation(item);
campaign.Money -= itemValue;
campaign.Map.CurrentLocation.StoreCurrentBalance += itemValue;
if (removeFromCrate)
{
// Remove from the shopping crate
var crateItem = ItemsInBuyCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab);
if (crateItem != null)
{
crateItem.Quantity -= item.Quantity;
if (crateItem.Quantity < 1) { ItemsInBuyCrate.Remove(crateItem); }
}
}
}
OnItemsChanged?.Invoke();
OnPurchasedItemsChanged?.Invoke();
}
public int GetTotalItemCost()
{
if (purchasedItems == null) return 0;
return purchasedItems.Sum(i => i.ItemPrefab.GetPrice(campaign.Map.CurrentLocation).BuyPrice * i.Quantity);
}
public int GetBuyValueAtCurrentLocation(PurchasedItem item) => item?.ItemPrefab != null && campaign?.Map?.CurrentLocation != null ?
item.Quantity* campaign.Map.CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab) : 0;
public void CreateItems()
public int GetSellValueAtCurrentLocation(ItemPrefab itemPrefab, int quantity = 1) => itemPrefab != null && campaign?.Map?.CurrentLocation != null ?
quantity * campaign.Map.CurrentLocation.GetAdjustedItemSellPrice(itemPrefab) : 0;
public void CreatePurchasedItems()
{
CreateItems(purchasedItems);
OnItemsChanged?.Invoke();
CreateItems(PurchasedItems);
OnPurchasedItemsChanged?.Invoke();
}
public static void CreateItems(List<PurchasedItem> itemsToSpawn)
@@ -110,7 +168,14 @@ namespace Barotrauma
}
#if CLIENT
new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true));
new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon");
#else
foreach (Client client in GameMain.Server.ConnectedClients)
{
ChatMessage msg = ChatMessage.Create("", $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}", ChatMessageType.ServerMessageBoxInGame, null);
msg.IconStyle = "StoreShoppingCrateIcon";
GameMain.Server.SendDirectChatMessage(msg, client);
}
#endif
Dictionary<ItemContainer, int> availableContainers = new Dictionary<ItemContainer, int>();
@@ -179,11 +244,12 @@ namespace Barotrauma
//no container, place at the waypoint
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, position, wp.Submarine);
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, position, wp.Submarine, onSpawned: itemSpawned);
}
else
{
new Item(pi.ItemPrefab, position, wp.Submarine);
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
itemSpawned(item);
}
continue;
}
@@ -205,12 +271,25 @@ namespace Barotrauma
//place in the container
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, itemContainer.Inventory);
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, itemContainer.Inventory, onSpawned: itemSpawned);
}
else
{
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
itemContainer.Inventory.TryPutItem(item, null);
itemSpawned(item);
}
static void itemSpawned(Item item)
{
Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine;
if (sub != null)
{
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = sub.TeamID;
}
}
}
//reduce the number of available slots in the container
@@ -227,5 +306,36 @@ namespace Barotrauma
}
itemsToSpawn.Clear();
}
public void SavePurchasedItems(XElement parentElement)
{
var itemsElement = new XElement("cargo");
foreach (PurchasedItem item in PurchasedItems)
{
if (item?.ItemPrefab == null) { continue; }
itemsElement.Add(new XElement("item",
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity)));
}
parentElement.Add(itemsElement);
}
public void LoadPurchasedItems(XElement element)
{
var purchasedItems = new List<PurchasedItem>();
if (element != null)
{
foreach (XElement itemElement in element.GetChildElements("item"))
{
var id = itemElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
if (prefab == null) { continue; }
var qty = itemElement.GetAttributeInt("qty", 0);
purchasedItems.Add(new PurchasedItem(prefab, qty));
}
}
SetPurchasedItems(purchasedItems);
}
}
}
@@ -1,6 +1,8 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -10,7 +12,16 @@ namespace Barotrauma
const float ConversationIntervalMax = 180.0f;
const float ConversationIntervalMultiplierMultiplayer = 5.0f;
private float conversationTimer, conversationLineTimer;
private List<Pair<Character, string>> pendingConversationLines = new List<Pair<Character, string>>();
private readonly List<Pair<Character, string>> pendingConversationLines = new List<Pair<Character, string>>();
private readonly List<CharacterInfo> characterInfos = new List<CharacterInfo>();
private readonly List<Character> characters = new List<Character>();
private Character welcomeMessageNPC;
public List<CharacterInfo> CharacterInfos => characterInfos;
public bool HasBots { get; set; }
public List<Pair<Order, float>> ActiveOrders { get; } = new List<Pair<Order, float>>();
public bool IsSinglePlayer { get; private set; }
@@ -51,6 +62,145 @@ namespace Barotrauma
ActiveOrders.RemoveAll(o => o.First == order);
}
public void AddCharacterElements(XElement element)
{
foreach (XElement subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; }
CharacterInfo characterInfo = new CharacterInfo(subElement);
#if CLIENT
if (subElement.GetAttributeBool("lastcontrolled", false)) { characterInfo.LastControlled = true; }
#endif
characterInfos.Add(characterInfo);
foreach (XElement invElement in subElement.Elements())
{
if (!invElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; }
characterInfo.InventoryData = invElement;
break;
}
}
}
/// <summary>
/// Remove info of a selected character. The character will not be visible in any menus or the round summary.
/// </summary>
/// <param name="characterInfo"></param>
public void RemoveCharacterInfo(CharacterInfo characterInfo)
{
characterInfos.Remove(characterInfo);
}
public void AddCharacter(Character character)
{
if (character.Removed)
{
DebugConsole.ThrowError("Tried to add a removed character to CrewManager!\n" + Environment.StackTrace);
return;
}
if (character.IsDead)
{
DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace);
return;
}
if (!characters.Contains(character))
{
characters.Add(character);
}
if (!characterInfos.Contains(character.Info))
{
characterInfos.Add(character.Info);
}
#if CLIENT
AddCharacterToCrewList(character);
DisplayCharacterOrder(character, character.CurrentOrder, character.CurrentOrderOption);
#endif
}
public void AddCharacterInfo(CharacterInfo characterInfo)
{
if (characterInfos.Contains(characterInfo))
{
DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace);
return;
}
characterInfos.Add(characterInfo);
}
public void InitRound()
{
characters.Clear();
List<WayPoint> spawnWaypoints = null;
List<WayPoint> mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub).ToList();
if (Level.IsLoadedOutpost)
{
spawnWaypoints = WayPoint.WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
wp.Submarine == Level.Loaded.StartOutpost &&
wp.CurrentHull?.OutpostModuleTags != null &&
wp.CurrentHull.OutpostModuleTags.Contains("airlock"));
while (spawnWaypoints.Count > characterInfos.Count)
{
spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count));
}
while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count)
{
spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]);
}
}
if (spawnWaypoints == null || !spawnWaypoints.Any())
{
spawnWaypoints = mainSubWaypoints;
}
System.Diagnostics.Debug.Assert(spawnWaypoints.Count == mainSubWaypoints.Count);
for (int i = 0; i < spawnWaypoints.Count; i++)
{
var info = characterInfos[i];
info.TeamID = Character.TeamType.Team1;
Character character = Character.Create(info, spawnWaypoints[i].WorldPosition, info.Name);
if (character.Info != null)
{
if (!character.Info.StartItemsGiven && character.Info.InventoryData != null)
{
DebugConsole.AddWarning($"Error when initializing a round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items.");
}
if (character.Info.InventoryData != null)
{
character.Info.SpawnInventoryItems(character.Inventory, character.Info.InventoryData);
}
else if (!character.Info.StartItemsGiven)
{
character.GiveJobItems(mainSubWaypoints[i]);
}
if (character.Info.HealthData != null)
{
character.Info.ApplyHealthData(character, character.Info.HealthData);
}
character.GiveIdCardTags(spawnWaypoints[i]);
character.Info.StartItemsGiven = true;
}
AddCharacter(character);
#if CLIENT
if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; }
#endif
}
conversationTimer = Rand.Range(5.0f, 10.0f);
}
public void FireCharacter(CharacterInfo characterInfo)
{
RemoveCharacterInfo(characterInfo);
}
public void Update(float deltaTime)
{
foreach (Pair<Order, float> order in ActiveOrders)
@@ -88,6 +238,49 @@ namespace Barotrauma
}
}
if (welcomeMessageNPC == null)
{
foreach (Character npc in Character.CharacterList)
{
if (npc.TeamID != Character.TeamType.FriendlyNPC || npc.CurrentHull == null || npc.IsIncapacitated) { continue; }
if (npc.AIController?.ObjectiveManager != null && (npc.AIController.ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>() || npc.AIController.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()))
{
continue;
}
foreach (Character player in Character.CharacterList)
{
if (player.TeamID != npc.TeamID && !player.IsIncapacitated && player.CurrentHull == npc.CurrentHull)
{
List<Character> availableSpeakers = new List<Character>() { npc, player };
List<string> dialogFlags = new List<string>() { "OutpostNPC", "EnterOutpost" };
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode && campaignMode.Map?.CurrentLocation?.Reputation != null)
{
float normalizedReputation = MathUtils.InverseLerp(
campaignMode.Map.CurrentLocation.Reputation.MinReputation,
campaignMode.Map.CurrentLocation.Reputation.MaxReputation,
campaignMode.Map.CurrentLocation.Reputation.Value);
if (normalizedReputation < 0.2f)
{
dialogFlags.Add("LowReputation");
}
else if (normalizedReputation > 0.8f)
{
dialogFlags.Add("HighReputation");
}
}
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers, dialogFlags));
welcomeMessageNPC = npc;
break;
}
}
if (welcomeMessageNPC != null) { break; }
}
}
else if (welcomeMessageNPC.Removed)
{
welcomeMessageNPC = null;
}
if (pendingConversationLines.Count > 0)
{
conversationLineTimer -= deltaTime;
@@ -0,0 +1,153 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma
{
internal partial class CampaignMetadata
{
public CampaignMode Campaign { get; }
private readonly Dictionary<string, object> data = new Dictionary<string, object>();
public CampaignMetadata(CampaignMode campaign)
{
Campaign = campaign;
}
public CampaignMetadata(CampaignMode campaign, XElement element)
{
Campaign = campaign;
foreach (XElement subElement in element.Elements())
{
if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase))
{
string identifier = subElement.GetAttributeString("key", string.Empty).ToLowerInvariant();
string value = subElement.GetAttributeString("value", string.Empty);
string valueType = subElement.GetAttributeString("type", string.Empty);
if (string.IsNullOrWhiteSpace(identifier) || string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(valueType))
{
DebugConsole.ThrowError("Unable to load value because one or more of the required attributes are empty.\n" +
$"key: \"{identifier}\", value: \"{value}\", type: \"{valueType}\"");
continue;
}
Type? type = Type.GetType(valueType);
if (type == null)
{
DebugConsole.ThrowError($"Type for {identifier} not found ({valueType}).");
continue;
}
if (type == typeof(float))
{
if (!float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatValue))
{
DebugConsole.ThrowError($"Error in campaign metadata: could not parse \"{value}\" as a float.");
continue;
}
data.Add(identifier, floatValue);
}
else
{
data.Add(identifier, Convert.ChangeType(value, type));
}
}
}
}
public void SetValue(string identifier, object value)
{
identifier = identifier.ToLowerInvariant();
DebugConsole.Log($"Set the value \"{identifier}\" to {value}");
if (!data.ContainsKey(identifier))
{
data.Add(identifier, value);
return;
}
data[identifier] = value;
}
public float GetFloat(string identifier, float? defaultValue = null)
{
return (float)GetTypeOrDefault(identifier, typeof(float), defaultValue ?? 0f);
}
public int GetInt(string identifier, int? defaultValue = null)
{
return (int)GetTypeOrDefault(identifier, typeof(int), defaultValue ?? 0);
}
public bool GetBoolean(string identifier, bool? defaultValue = null)
{
return (bool)GetTypeOrDefault(identifier, typeof(bool), defaultValue ?? false);
}
public string GetString(string identifier, string? defaultValue = null)
{
return (string)GetTypeOrDefault(identifier, typeof(string), defaultValue ?? string.Empty);
}
public bool HasKey(string identifier)
{
identifier = identifier.ToLowerInvariant();
return data.ContainsKey(identifier);
}
private object GetTypeOrDefault(string identifier, Type type, object defaultValue)
{
object? value = GetValue(identifier);
if (value == null)
{
SetValue(identifier, defaultValue);
}
else if (value.GetType() == type)
{
return value;
}
else
{
DebugConsole.ThrowError($"Attempted to get value \"{identifier}\" as a {type} but the value is {value.GetType()}.");
}
return defaultValue;
}
public object? GetValue(string identifier)
{
return data.ContainsKey(identifier) ? data[identifier] : null;
}
public void Save(XElement modeElement)
{
XElement element = new XElement("Metadata");
foreach (var (key, value) in data)
{
string valueStr = value?.ToString() ?? "";
if (value?.GetType() == typeof(float))
{
valueStr = ((float)value).ToString("G", CultureInfo.InvariantCulture);
}
element.Add(new XElement("Data",
new XAttribute("key", key),
new XAttribute("value", valueStr),
new XAttribute("type", value?.GetType())));
}
#if DEBUG || UNSTABLE
DebugConsole.Log(element.ToString());
#endif
modeElement.Add(element);
}
}
}
@@ -0,0 +1,141 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class Faction
{
public Reputation Reputation { get; }
public FactionPrefab Prefab { get; }
public Faction(CampaignMetadata metadata, FactionPrefab prefab)
{
Prefab = prefab;
Reputation = new Reputation(metadata, $"faction.{prefab.Identifier}", prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation);
}
}
internal class FactionPrefab : IDisposable
{
public static List<FactionPrefab> Prefabs { get; set; }
public string Name { get; }
public string Description { get; }
public string ShortDescription { get; }
public string Identifier { get; }
/// <summary>
/// How low the reputation can drop on this faction
/// </summary>
public int MinReputation { get; }
/// <summary>
/// Maximum reputation level you can gain on this faction
/// </summary>
public int MaxReputation { get; }
/// <summary>
/// What reputation does this faction start with
/// </summary>
public int InitialReputation { get; }
#if CLIENT
public Sprite? Icon { get; private set; }
public Sprite? BackgroundPortrait { get; private set; }
public Color IconColor { get; }
#endif
private FactionPrefab(XElement element)
{
Identifier = element.GetAttributeString("identifier", string.Empty);
MinReputation = element.GetAttributeInt("minreputation", -100);
MaxReputation = element.GetAttributeInt("maxreputation", 100);
InitialReputation = element.GetAttributeInt("initialreputation", 0);
Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}", returnNull: true) ?? "Unnamed";
Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description", returnNull: true) ?? "";
ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription", returnNull: true) ?? "";
#if CLIENT
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase))
{
IconColor = subElement.GetAttributeColor("color", Color.White);
Icon = new Sprite(subElement);
}
else if (subElement.Name.ToString().Equals("portrait", StringComparison.OrdinalIgnoreCase))
{
BackgroundPortrait = new Sprite(subElement);
}
}
#endif
}
public static void LoadFactions()
{
Prefabs?.ForEach(set => set.Dispose());
Prefabs = new List<FactionPrefab>();
IEnumerable<ContentFile> files = GameMain.Instance.GetFilesOfType(ContentType.Factions);
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
XElement? rootElement = doc?.Root;
if (doc == null || rootElement == null) { continue; }
if (doc.Root.IsOverride())
{
Prefabs.Clear();
DebugConsole.NewMessage($"Overriding all factions with '{file.Path}'", Color.Yellow);
}
foreach (XElement element in rootElement.Elements())
{
bool isOverride = element.IsOverride();
XElement sourceElement = isOverride ? element.FirstElement() : element;
string elementName = sourceElement.Name.ToString().ToLowerInvariant();
string identifier = sourceElement.GetAttributeString("identifier", null);
if (string.IsNullOrWhiteSpace(identifier))
{
DebugConsole.ThrowError($"No identifier defined for the faction config '{elementName}' in file '{file.Path}'");
continue;
}
var existingParams = Prefabs.Find(set => set.Identifier == identifier);
if (existingParams != null)
{
if (isOverride)
{
DebugConsole.NewMessage($"Overriding faction config '{identifier}' using the file '{file.Path}'", Color.Yellow);
Prefabs.Remove(existingParams);
}
else
{
DebugConsole.ThrowError($"Duplicate faction config: '{identifier}' defined in {elementName} of '{file.Path}'");
continue;
}
}
Prefabs.Add(new FactionPrefab(element));
}
}
}
public void Dispose()
{
#if CLIENT
Icon?.Remove();
Icon = null;
#endif
GC.SuppressFinalize(this);
}
}
}
@@ -0,0 +1,46 @@
using System;
namespace Barotrauma
{
class Reputation
{
public const float HostileThreshold = 0.1f;
public const float ReputationLossPerNPCDamage = 0.1f;
public const float ReputationLossPerStolenItemPrice = 0.01f;
public const float MinReputationLossPerStolenItem = 0.5f;
public const float MaxReputationLossPerStolenItem = 10.0f;
public string Identifier { get; }
public int MinReputation { get; }
public int MaxReputation { get; }
public int InitialReputation { get; }
public CampaignMetadata Metadata { get; }
private readonly string metaDataIdentifier;
/// <summary>
/// Reputation value normalized to the range of 0-1
/// </summary>
public float NormalizedValue
{
get { return MathUtils.InverseLerp(MinReputation, MaxReputation, Value); }
}
public float Value
{
get => Math.Min(MaxReputation, Metadata.GetFloat(metaDataIdentifier, InitialReputation));
set => Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation));
}
public Reputation(CampaignMetadata metadata, string identifier, int minReputation, int maxReputation, int initialReputation)
{
System.Diagnostics.Debug.Assert(metadata != null);
Metadata = metadata;
Identifier = identifier.ToLowerInvariant();
metaDataIdentifier = $"reputation.{Identifier}";
MinReputation = minReputation;
MaxReputation = maxReputation;
InitialReputation = initialReputation;
}
}
}
@@ -1,4 +1,6 @@
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -8,22 +10,62 @@ namespace Barotrauma
{
abstract partial class CampaignMode : GameMode
{
const int MaxMoney = int.MaxValue / 2; //about 1 billion
const int InitialMoney = 2500;
public const int MaxInitialSubmarinePrice = 6000;
//duration of the cinematic + credits at the end of the campaign
protected const float EndCinematicDuration = 240.0f;
//duration of the camera transition at the end of a round
protected const float EndTransitionDuration = 5.0f;
//there can be no events before this time has passed during the 1st campaign round
const float FirstRoundEventDelay = 30.0f;
public enum InteractionType { None, Talk, Map, Crew, Store, Repair, Upgrade, PurchaseSub }
public readonly CargoManager CargoManager;
public UpgradeManager UpgradeManager;
public List<Faction> Factions;
public CampaignMetadata CampaignMetadata;
public enum TransitionType
{
None,
//leaving a location level
LeaveLocation,
//progressing to next location level
ProgressToNextLocation,
//returning to previous location level
ReturnToPreviousLocation,
//returning to previous location (one with no level/outpost, the player is taken to the map screen and must choose their next destination)
ReturnToPreviousEmptyLocation,
//progressing to an empty location (one with no level/outpost, the player is taken to the map screen and must choose their next destination)
ProgressToNextEmptyLocation,
//end of campaign (reached end location)
End
}
public bool IsFirstRound { get; protected set; } = true;
public bool DisableEvents
{
get { return IsFirstRound && Timing.TotalTime < GameMain.GameSession.RoundStartTime + FirstRoundEventDelay; }
}
public bool CheatsEnabled;
const int InitialMoney = 8700;
public const int HullRepairCost = 500, ItemRepairCost = 500, ShuttleReplaceCost = 1000;
protected bool watchmenSpawned;
protected Character startWatchman, endWatchman;
protected bool wasDocked;
//key = dialog flag, double = Timing.TotalTime when the line was last said
private Dictionary<string, double> dialogLastSpoken = new Dictionary<string, double>();
private readonly Dictionary<string, double> dialogLastSpoken = new Dictionary<string, double>();
public bool PurchasedHullRepairs, PurchasedLostShuttles, PurchasedItemRepairs;
public bool InitialSuppliesSpawned;
public SubmarineInfo PendingSubmarineSwitch;
protected Map map;
public Map Map
@@ -43,28 +85,47 @@ namespace Barotrauma
public int Money
{
get { return money; }
set { money = Math.Max(value, 0); }
set { money = MathHelper.Clamp(value, 0, MaxMoney); }
}
public CampaignMode(GameModePreset preset, object param)
: base(preset, param)
public LevelData NextLevel
{
get;
protected set;
}
protected CampaignMode(GameModePreset preset)
: base(preset)
{
Money = InitialMoney;
CargoManager = new CargoManager(this);
CargoManager = new CargoManager(this);
}
public void GenerateMap(string seed)
/// <summary>
/// The location that's displayed as the "current one" in the map screen. Normally the current outpost or the location at the start of the level,
/// but when selecting the next destination at the end of the level at an uninhabited location we use the location at the end
/// </summary>
public Location CurrentDisplayLocation
{
map = new Map(seed);
get
{
if (Level.Loaded != null && !Level.Loaded.Generating &&
Level.Loaded.Type == LevelData.LevelType.LocationConnection &&
GetAvailableTransition(out _, out _) == TransitionType.ProgressToNextEmptyLocation)
{
return Level.Loaded.EndLocation;
}
return Level.Loaded?.StartLocation ?? Map.CurrentLocation;
}
}
protected List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
public List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
{
//leave subs behind if they're not docked to the leaving sub and not at the same exit
return Submarine.Loaded.FindAll(s =>
s != leavingSub &&
!leavingSub.DockedTo.Contains(s) &&
s.Info.Type == SubmarineInfo.SubmarineType.Player &&
s.Info.Type == SubmarineType.Player &&
(s.AtEndPosition != leavingSub.AtEndPosition || s.AtStartPosition != leavingSub.AtStartPosition));
}
@@ -72,20 +133,18 @@ namespace Barotrauma
{
base.Start();
dialogLastSpoken.Clear();
watchmenSpawned = false;
startWatchman = null;
endWatchman = null;
characterOutOfBoundsTimer.Clear();
if (PurchasedHullRepairs)
{
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine == null || wall.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (wall.Submarine == null || wall.Submarine.Info.Type != SubmarineType.Player) { continue; }
if (wall.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(wall.Submarine))
{
for (int i = 0; i < wall.SectionCount; i++)
{
wall.AddDamage(i, -wall.Prefab.Health);
wall.AddDamage(i, -wall.MaxHealth);
}
}
}
@@ -95,125 +154,539 @@ namespace Barotrauma
{
foreach (Item item in Item.ItemList)
{
if (item.Submarine == null || item.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (item.Submarine == null || item.Submarine.Info.Type != SubmarineType.Player) { continue; }
if (item.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(item.Submarine))
{
if (item.GetComponent<Items.Components.Repairable>() != null)
{
item.Condition = item.Prefab.Health;
item.Condition = item.MaxCondition;
}
}
}
PurchasedItemRepairs = false;
}
PurchasedLostShuttles = false;
var connectedSubs = Submarine.MainSub.GetConnectedSubs();
wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost);
}
public override void Update(float deltaTime)
public void InitCampaignData()
{
base.Update(deltaTime);
if (!IsRunning) { return; }
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (!watchmenSpawned)
Factions = new List<Faction>();
foreach (FactionPrefab factionPrefab in FactionPrefab.Prefabs)
{
if (Level.Loaded.StartOutpost != null) { startWatchman = SpawnWatchman(Level.Loaded.StartOutpost); }
if (Level.Loaded.EndOutpost != null) { endWatchman = SpawnWatchman(Level.Loaded.EndOutpost); }
watchmenSpawned = true;
#if SERVER
(this as MultiPlayerCampaign).LastUpdateID++;
Factions.Add(new Faction(CampaignMetadata, factionPrefab));
}
}
public void LoadNewLevel()
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return;
}
if (CoroutineManager.IsCoroutineRunning("LevelTransition"))
{
DebugConsole.ThrowError("Level transition already running.\n" + Environment.StackTrace);
return;
}
if (Level.Loaded == null || Submarine.MainSub == null)
{
LoadInitialLevel();
return;
}
var availableTransition = GetAvailableTransition(out LevelData nextLevel, out Submarine leavingSub);
if (availableTransition == TransitionType.None)
{
DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " +
"(current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" +
Environment.StackTrace);
return;
}
if (nextLevel == null)
{
DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " +
"(transition type: " + availableTransition + ", " +
"current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" +
Environment.StackTrace);
return;
}
#if CLIENT
ShowCampaignUI = ForceMapUI = false;
#endif
DebugConsole.NewMessage("Transitioning to " + (nextLevel?.Seed ?? "null") +
" (current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ", " +
"transition type: " + availableTransition + ")");
IsFirstRound = false;
bool mirror = map.SelectedConnection != null && map.CurrentLocation != map.SelectedConnection.Locations[0];
CoroutineManager.StartCoroutine(DoLevelTransition(availableTransition, nextLevel, leavingSub, mirror), "LevelTransition");
}
/// <summary>
/// Load the first level and start the round after loading a save file
/// </summary>
protected abstract void LoadInitialLevel();
protected abstract IEnumerable<object> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List<TraitorMissionResult> traitorResults = null);
/// <summary>
/// Which type of transition between levels is currently possible (if any)
/// </summary>
public TransitionType GetAvailableTransition(out LevelData nextLevel, out Submarine leavingSub)
{
if (Level.Loaded == null || Submarine.MainSub == null)
{
nextLevel = null;
leavingSub = null;
return TransitionType.None;
}
leavingSub = GetLeavingSub();
if (leavingSub == null)
{
nextLevel = null;
return TransitionType.None;
}
//currently travelling from location to another
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
if (leavingSub.AtEndPosition)
{
if (Map.EndLocation != null && map.SelectedLocation == Map.EndLocation)
{
nextLevel = map.StartLocation.LevelData;
return TransitionType.End;
}
if (Level.Loaded.EndLocation != null && Level.Loaded.EndLocation.Type.HasOutpost && Level.Loaded.EndOutpost != null)
{
nextLevel = Level.Loaded.EndLocation.LevelData;
return TransitionType.ProgressToNextLocation;
}
else if (map.SelectedConnection != null)
{
nextLevel = Level.Loaded.LevelData != map.SelectedConnection?.LevelData || (map.SelectedConnection.Locations[0] == Level.Loaded.EndLocation == Level.Loaded.Mirrored) ?
map.SelectedConnection.LevelData : null;
return TransitionType.ProgressToNextEmptyLocation;
}
else
{
nextLevel = null;
return TransitionType.ProgressToNextEmptyLocation;
}
}
else if (leavingSub.AtStartPosition)
{
if (map.CurrentLocation.Type.HasOutpost && Level.Loaded.StartOutpost != null)
{
nextLevel = map.CurrentLocation.LevelData;
return TransitionType.ReturnToPreviousLocation;
}
else if (map.SelectedLocation != null && map.SelectedLocation != map.CurrentLocation && !map.CurrentLocation.Type.HasOutpost &&
(Level.Loaded.LevelData != map.SelectedConnection.LevelData))
{
nextLevel = map.SelectedConnection.LevelData;
return TransitionType.LeaveLocation;
}
else
{
nextLevel = map.SelectedConnection?.LevelData;
return TransitionType.ReturnToPreviousEmptyLocation;
}
}
else
{
nextLevel = null;
return TransitionType.None;
}
}
else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
{
nextLevel = map.SelectedLocation == null ? null : map.SelectedConnection?.LevelData;
return nextLevel == null ? TransitionType.None : TransitionType.LeaveLocation;
}
else
{
foreach (Character character in Character.CharacterList)
throw new NotImplementedException();
}
}
/// <summary>
/// Which submarine is at a position where it can leave the level and enter another one (if any).
/// </summary>
private Submarine GetLeavingSub()
{
//in single player, only the sub the controlled character is inside can transition between levels
//in multiplayer, if there's subs at both ends of the level, only the one with more players inside can transition
//TODO: ignore players who don't have the permission to trigger a transition between levels?
var leavingPlayers = Character.CharacterList.Where(c => !c.IsDead && (c == Character.Controlled || c.IsRemotePlayer));
//allow leaving if inside an outpost, and the submarine is either docked to it or close enough
Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers);
Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers);
if (Level.IsLoadedOutpost)
{
leavingSubAtStart ??= Submarine.MainSub;
leavingSubAtEnd ??= Submarine.MainSub;
}
int playersInSubAtStart = leavingSubAtStart == null ? 0 :
leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost));
int playersInSubAtEnd = leavingSubAtEnd == null ? 0 :
leavingPlayers.Count(c => c.Submarine == leavingSubAtEnd || leavingSubAtEnd.DockedTo.Contains(c.Submarine) || (Level.Loaded.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost));
if (playersInSubAtStart == 0 && playersInSubAtEnd == 0) { return null; }
return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd;
static Submarine GetLeavingSubAtStart(IEnumerable<Character> leavingPlayers)
{
if (Level.Loaded.StartOutpost == null)
{
#if SERVER
if (string.IsNullOrEmpty(character.OwnerClientEndPoint)) { continue; }
#else
if (!CrewManager.GetCharacters().Contains(character)) { continue; }
#endif
if (character.Submarine == Level.Loaded.StartOutpost &&
Vector2.DistanceSquared(character.WorldPosition, startWatchman.WorldPosition) < 500.0f * 500.0f)
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartPosition, ignoreOutposts: true);
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
else
{
//if there's a sub docked to the outpost, we can leave the level
if (Level.Loaded.StartOutpost.DockedTo.Any())
{
CreateDialog(new List<Character> { startWatchman }, "EnterStartOutpost", 5 * 60.0f);
var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault();
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
}
else if (character.Submarine == Level.Loaded.EndOutpost &&
Vector2.DistanceSquared(character.WorldPosition, endWatchman.WorldPosition) < 500.0f * 500.0f)
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; }
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true);
if (closestSub == null || !closestSub.AtStartPosition) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
}
static Submarine GetLeavingSubAtEnd(IEnumerable<Character> leavingPlayers)
{
//no "end" in outpost levels
if (Level.Loaded.Type == LevelData.LevelType.Outpost) { return null; }
if (Level.Loaded.EndOutpost == null)
{
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndPosition, ignoreOutposts: true);
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
else
{
//if there's a sub docked to the outpost, we can leave the level
if (Level.Loaded.EndOutpost.DockedTo.Any())
{
CreateDialog(new List<Character> { endWatchman }, "EnterEndOutpost", 5 * 60.0f);
var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault();
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
}
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; }
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true);
if (closestSub == null || !closestSub.AtEndPosition) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
}
}
protected void CreateDialog(List<Character> speakers, string conversationTag, float minInterval)
public override void End(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
{
if (dialogLastSpoken.TryGetValue(conversationTag, out double lastTime))
List<Item> takenItems = new List<Item>();
foreach (Item item in Item.ItemList)
{
if (Timing.TotalTime - lastTime < minInterval) { return; }
}
CrewManager.AddConversation(
NPCConversation.CreateRandom(speakers, new List<string>() { conversationTag }));
dialogLastSpoken[conversationTag] = Timing.TotalTime;
}
private Character SpawnWatchman(Submarine outpost)
{
WayPoint watchmanSpawnpoint = WayPoint.WayPointList.Find(wp => wp.Submarine == outpost);
if (watchmanSpawnpoint == null)
{
DebugConsole.ThrowError("Failed to spawn a watchman at the outpost. No spawnpoints found inside the outpost.");
return null;
}
string seed = outpost == Level.Loaded.StartOutpost ? map.SelectedLocation.Name : map.CurrentLocation.Name;
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
JobPrefab watchmanJob = JobPrefab.Get("watchman");
var variant = Rand.Range(0, watchmanJob.Variants, Rand.RandSync.Server);
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: watchmanJob, variant: variant);
var spawnedCharacter = Character.Create(characterInfo, watchmanSpawnpoint.WorldPosition,
Level.Loaded.Seed + (outpost == Level.Loaded.StartOutpost ? "start" : "end"));
InitializeWatchman(spawnedCharacter);
var objectiveManager = (spawnedCharacter.AIController as HumanAIController)?.ObjectiveManager;
if (objectiveManager != null)
{
var moveOrder = new AIObjectiveGoTo(watchmanSpawnpoint, spawnedCharacter, objectiveManager, repeat: true, getDivingGearIfNeeded: false);
moveOrder.Completed += () =>
if (!item.SpawnedInOutpost || item.OriginalModuleIndex < 0) { continue; }
if ((!(item.GetRootInventoryOwner()?.Submarine?.Info?.IsOutpost ?? false)) || item.Submarine == null || !item.Submarine.Info.IsOutpost)
{
// Turn towards the center of the sub. Doesn't work in all possible cases, but this is the simplest solution for now.
spawnedCharacter.AnimController.TargetDir = spawnedCharacter.Submarine.WorldPosition.X > spawnedCharacter.WorldPosition.X ? Direction.Right : Direction.Left;
};
objectiveManager.SetOrder(moveOrder);
takenItems.Add(item);
}
}
if (watchmanJob != null)
map.CurrentLocation.RegisterTakenItems(takenItems);
map.CurrentLocation.AddToStock(CargoManager.SoldItems);
CargoManager.ClearSoldItemsProjSpecific();
map.CurrentLocation.RemoveFromStock(CargoManager.PurchasedItems);
if (GameMain.NetworkMember == null)
{
spawnedCharacter.GiveJobItems();
CargoManager.ClearItemsInBuyCrate();
CargoManager.ClearItemsInSellCrate();
}
else
{
if (GameMain.NetworkMember.IsServer)
{
CargoManager.ClearItemsInBuyCrate();
}
else if (GameMain.NetworkMember.IsClient)
{
CargoManager.ClearItemsInSellCrate();
}
}
if (Level.Loaded?.StartOutpost != null)
{
List<Character> killedCharacters = new List<Character>();
foreach (Character c in Level.Loaded.StartOutpost.Info.OutpostNPCs.SelectMany(kpv => kpv.Value))
{
if (!c.IsDead && !c.Removed) { continue; }
killedCharacters.Add(c);
}
map.CurrentLocation.RegisterKilledCharacters(killedCharacters);
Level.Loaded.StartOutpost.Info.OutpostNPCs.Clear();
}
List<Character> deadCharacters = Character.CharacterList.FindAll(c => c.IsDead);
foreach (Character c in deadCharacters)
{
if (c.IsDead)
{
CrewManager.RemoveCharacterInfo(c.Info);
c.DespawnNow();
}
}
foreach (CharacterInfo ci in CrewManager.CharacterInfos)
{
ci?.ResetCurrentOrder();
}
foreach (DockingPort port in DockingPort.List)
{
if (port.Door != null &
port.Item.Submarine.Info.Type == SubmarineType.Player &&
port.DockingTarget?.Item?.Submarine != null &&
port.DockingTarget.Item.Submarine.Info.IsOutpost)
{
port.Door.IsOpen = false;
}
}
return spawnedCharacter;
}
protected void InitializeWatchman(Character character)
public void EndCampaign()
{
foreach (LocationConnection connection in Map.Connections)
{
connection.Difficulty = MathHelper.Lerp(connection.Difficulty, 100.0f, 0.25f);
connection.LevelData.Difficulty = connection.Difficulty;
}
foreach (Location location in Map.Locations)
{
location.CreateStore(force: true);
location.ClearMissions();
}
Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation));
Map.SelectLocation(-1);
EndCampaignProjSpecific();
}
protected virtual void EndCampaignProjSpecific() { }
public bool TryHireCharacter(Location location, CharacterInfo characterInfo)
{
if (Money < characterInfo.Salary) { return false; }
characterInfo.IsNewHire = true;
location.RemoveHireableCharacter(characterInfo);
CrewManager.AddCharacterInfo(characterInfo);
Money -= characterInfo.Salary;
return true;
}
private void NPCInteract(Character npc, Character interactor)
{
if (!npc.AllowCustomInteract) { return; }
NPCInteractProjSpecific(npc, interactor);
string coroutineName = "DoCharacterWait." + (npc?.ID ?? Entity.NullEntityID);
if (!CoroutineManager.IsCoroutineRunning(coroutineName))
{
CoroutineManager.StartCoroutine(DoCharacterWait(npc, interactor), coroutineName);
}
}
private IEnumerable<object> DoCharacterWait(Character npc, Character interactor)
{
if (npc == null || interactor == null) { yield return CoroutineStatus.Failure; }
HumanAIController humanAI = npc.AIController as HumanAIController;
if (humanAI == null) { yield return CoroutineStatus.Failure; }
OrderInfo? prevSpeakerOrder = null;
if (humanAI.CurrentOrder != null)
{
prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption);
}
var waitOrder = Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase));
humanAI.SetOrder(waitOrder, option: string.Empty, orderGiver: null, speak: false);
humanAI.FaceTarget(interactor);
while (!npc.Removed && !interactor.Removed &&
Vector2.DistanceSquared(npc.WorldPosition, interactor.WorldPosition) < 300.0f * 300.0f &&
humanAI.CurrentOrder == waitOrder &&
humanAI.AllowCampaignInteraction() &&
!interactor.IsIncapacitated)
{
yield return CoroutineStatus.Running;
}
#if CLIENT
ShowCampaignUI = false;
#endif
if (humanAI.CurrentOrder == waitOrder)
{
if (prevSpeakerOrder != null)
{
humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false);
}
else
{
humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false);
}
}
yield return CoroutineStatus.Success;
}
partial void NPCInteractProjSpecific(Character npc, Character interactor);
public void AssignNPCMenuInteraction(Character character, InteractionType interactionType)
{
character.CampaignInteractionType = interactionType;
if (interactionType == InteractionType.None)
{
character.SetCustomInteract(null, null);
return;
}
character.CharacterHealth.UseHealthWindow = false;
character.CharacterHealth.Unkillable = true;
character.CanInventoryBeAccessed = false;
character.CanBeDragged = false;
character.TeamID = Character.TeamType.FriendlyNPC;
//character.CanInventoryBeAccessed = false;
character.SetCustomInteract(
WatchmanInteract,
#if CLIENT
hudText: TextManager.GetWithVariable("TalkHint", "[key]", GameMain.Config.KeyBindText(InputType.Select)));
NPCInteract,
#if CLIENT
hudText: TextManager.GetWithVariable("CampaignInteraction." + interactionType, "[key]", GameMain.Config.KeyBindText(InputType.Use)));
#else
hudText: TextManager.Get("TalkHint"));
hudText: TextManager.Get("CampaignInteraction." + interactionType));
#endif
}
protected abstract void WatchmanInteract(Character watchman, Character interactor);
private readonly Dictionary<Character, float> characterOutOfBoundsTimer = new Dictionary<Character, float>();
protected void KeepCharactersCloseToOutpost(float deltaTime)
{
const float MaxDist = 3000.0f;
const float MinDist = 2500.0f;
if (!Level.IsLoadedOutpost) { return; }
Rectangle worldBorders = Submarine.MainSub.GetDockedBorders();
worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint();
foreach (Character c in Character.CharacterList)
{
if ((c != Character.Controlled && !c.IsRemotePlayer) ||
c.Removed || c.IsDead || c.IsIncapacitated || c.Submarine != null)
{
if (characterOutOfBoundsTimer.ContainsKey(c))
{
c.OverrideMovement = null;
characterOutOfBoundsTimer.Remove(c);
}
continue;
}
if (c.WorldPosition.Y < worldBorders.Y - worldBorders.Height - MaxDist)
{
if (!characterOutOfBoundsTimer.ContainsKey(c))
{
characterOutOfBoundsTimer.Add(c, 0.0f);
}
else
{
characterOutOfBoundsTimer[c] += deltaTime;
}
}
else if (c.WorldPosition.Y > worldBorders.Y - worldBorders.Height - MinDist)
{
if (characterOutOfBoundsTimer.ContainsKey(c))
{
c.OverrideMovement = null;
characterOutOfBoundsTimer.Remove(c);
}
}
}
foreach (KeyValuePair<Character, float> character in characterOutOfBoundsTimer)
{
if (character.Value <= 0.0f)
{
if (IsSinglePlayer)
{
#if CLIENT
GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(
TextManager.Get("RadioAnnouncerName"),
TextManager.Get("TooFarFromOutpostWarning"),
Networking.ChatMessageType.Default,
sender: null);
#endif
}
else
{
#if SERVER
foreach (Networking.Client c in GameMain.Server.ConnectedClients)
{
GameMain.Server.SendDirectChatMessage(Networking.ChatMessage.Create(
TextManager.Get("RadioAnnouncerName"),
TextManager.Get("TooFarFromOutpostWarning"), Networking.ChatMessageType.Default, null), c);
}
#endif
}
}
character.Key.OverrideMovement = Vector2.UnitY * 10.0f;
#if CLIENT
Character.DisableControls = true;
#endif
//if the character doesn't get back up in 10 seconds (something blocking the way?), teleport it closer
if (character.Value > 10.0f)
{
Vector2 teleportPos = character.Key.WorldPosition;
teleportPos += Vector2.Normalize(Submarine.MainSub.WorldPosition - character.Key.WorldPosition) * 100.0f;
character.Key.AnimController.SetPosition(ConvertUnits.ToSimUnits(teleportPos));
}
}
}
public void OutpostNPCAttacked(Character npc, Character attacker, AttackResult attackResult)
{
if (npc == null || attacker == null || npc.IsDead || npc.TurnedHostileByEvent) { return; }
if (npc.TeamID != Character.TeamType.FriendlyNPC) { return; }
if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; }
Location location = Map?.CurrentLocation;
if (location != null)
{
location.Reputation.Value -= attackResult.Damage * Reputation.ReputationLossPerNPCDamage;
}
}
public abstract void Save(XElement element);
public void LogState()
@@ -25,6 +25,7 @@ namespace Barotrauma
}
private XElement itemData;
private XElement healthData;
partial void InitProjSpecific(Client client);
public CharacterCampaignData(Client client)
@@ -32,6 +33,8 @@ namespace Barotrauma
Name = client.Name;
InitProjSpecific(client);
healthData = new XElement("health");
client.Character.CharacterHealth.Save(healthData);
if (client.Character.Inventory != null)
{
itemData = new XElement("inventory");
@@ -61,10 +64,24 @@ namespace Barotrauma
case "inventory":
itemData = subElement;
break;
case "health":
healthData = subElement;
break;
}
}
}
public void Refresh(Character character)
{
healthData = new XElement("health");
character.CharacterHealth.Save(healthData);
if (character.Inventory != null)
{
itemData = new XElement("inventory");
character.SaveInventory(character.Inventory, itemData);
}
}
public XElement Save()
{
XElement element = new XElement("CharacterCampaignData",
@@ -73,11 +90,8 @@ namespace Barotrauma
new XAttribute("steamid", SteamID));
CharacterInfo?.Save(element);
if (itemData != null)
{
element.Add(itemData);
}
if (itemData != null) { element.Add(itemData); }
if (healthData != null) { element.Add(healthData); }
return element;
}
@@ -8,14 +8,10 @@ namespace Barotrauma
public static List<GameModePreset> PresetList = new List<GameModePreset>();
protected DateTime startTime;
protected bool isRunning;
protected GameModePreset preset;
private string endMessage;
protected CrewManager CrewManager
public CrewManager CrewManager
{
get { return GameMain.GameSession?.CrewManager; }
}
@@ -25,11 +21,6 @@ namespace Barotrauma
get { return null; }
}
public bool IsRunning
{
get { return isRunning; }
}
public bool IsSinglePlayer
{
get { return preset.IsSinglePlayer; }
@@ -40,9 +31,9 @@ namespace Barotrauma
get { return preset.Name; }
}
public string EndMessage
public virtual bool Paused
{
get { return endMessage; }
get { return false; }
}
public GameModePreset Preset
@@ -50,7 +41,7 @@ namespace Barotrauma
get { return preset; }
}
public GameMode(GameModePreset preset, object param)
public GameMode(GameModePreset preset)
{
this.preset = preset;
}
@@ -58,10 +49,6 @@ namespace Barotrauma
public virtual void Start()
{
startTime = DateTime.Now;
endMessage = "The round has ended!";
isRunning = true;
}
public virtual void ShowStartMessage() { }
@@ -69,8 +56,6 @@ namespace Barotrauma
public virtual void AddToGUIUpdateList()
{
#if CLIENT
if (!isRunning) return;
GameMain.GameSession?.CrewManager.AddToGUIUpdateList();
#endif
}
@@ -80,15 +65,10 @@ namespace Barotrauma
CrewManager?.Update(deltaTime);
}
public virtual void End(string endMessage = "")
public virtual void End(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
{
isRunning = false;
if (endMessage != "" || this.endMessage == null) this.endMessage = endMessage;
GameMain.GameSession.EndRound(endMessage);
}
public virtual void Remove() { }
}
}
@@ -8,7 +8,15 @@ namespace Barotrauma
{
public static List<GameModePreset> List = new List<GameModePreset>();
public readonly ConstructorInfo Constructor;
public static GameModePreset SinglePlayerCampaign;
public static GameModePreset MultiPlayerCampaign;
public static GameModePreset Tutorial;
public static GameModePreset Mission;
public static GameModePreset TestMode;
public static GameModePreset Sandbox;
public static GameModePreset DevSandbox;
public readonly Type GameModeType;
public readonly string Name;
public readonly string Description;
@@ -26,7 +34,7 @@ namespace Barotrauma
Description = TextManager.Get("GameModeDescription." + identifier, returnNull: true) ?? "";
Identifier = identifier;
Constructor = type.GetConstructor(new Type[] { typeof(GameModePreset), typeof(object) });
GameModeType = type;
IsSinglePlayer = isSinglePlayer;
Votable = votable;
@@ -34,23 +42,17 @@ namespace Barotrauma
List.Add(this);
}
public GameMode Instantiate(object param)
{
object[] lobject = new object[] { this, param };
return (GameMode)Constructor.Invoke(lobject);
}
public static void Init()
{
#if CLIENT
new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true);
new GameModePreset("subtest", typeof(SubTestMode), true);
new GameModePreset("tutorial", typeof(TutorialMode), true);
new GameModePreset("devsandbox", typeof(GameMode), true);
Tutorial = new GameModePreset("tutorial", typeof(TutorialMode), true);
DevSandbox = new GameModePreset("devsandbox", typeof(GameMode), true);
SinglePlayerCampaign = new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true);
TestMode = new GameModePreset("testmode", typeof(TestGameMode), true);
#endif
new GameModePreset("sandbox", typeof(GameMode), false);
new GameModePreset("mission", typeof(MissionMode), false);
new GameModePreset("multiplayercampaign", typeof(MultiPlayerCampaign), false, false);
Sandbox = new GameModePreset("sandbox", typeof(GameMode), false);
Mission = new GameModePreset("mission", typeof(MissionMode), false);
MultiPlayerCampaign = new GameModePreset("multiplayercampaign", typeof(MultiPlayerCampaign), false, false);
}
}
}
@@ -2,7 +2,7 @@
{
partial class MissionMode : GameMode
{
private Mission mission;
private readonly Mission mission;
public override Mission Mission
{
@@ -12,26 +12,18 @@
}
}
public MissionMode(GameModePreset preset, object param)
: base(preset, param)
public MissionMode(GameModePreset preset, MissionPrefab missionPrefab)
: base(preset)
{
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
if (param is MissionType missionType)
{
mission = Mission.LoadRandom(locations, GameMain.NetLobbyScreen.LevelSeed, false, missionType);
}
else if (param is MissionPrefab missionPrefab)
{
mission = missionPrefab.Instantiate(locations);
}
else if (param is Mission)
{
mission = (Mission)param;
}
else
{
throw new System.ArgumentException("Unrecognized MissionMode parameter \"" + param + "\"");
}
mission = missionPrefab.Instantiate(locations);
}
public MissionMode(GameModePreset preset, MissionType missionType, string seed)
: base(preset)
{
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
mission = Mission.LoadRandom(locations, seed, false, missionType);
}
}
}
@@ -1,10 +1,8 @@
using Barotrauma.Networking;
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -16,7 +14,7 @@ namespace Barotrauma
get
{
#if SERVER
if (GameMain.Server != null && lastUpdateID < 1) lastUpdateID++;
if (GameMain.Server != null && lastUpdateID < 1) { lastUpdateID++; }
#endif
return lastUpdateID;
}
@@ -29,141 +27,59 @@ namespace Barotrauma
get
{
#if SERVER
if (GameMain.Server != null && lastSaveID < 1) lastSaveID++;
if (GameMain.Server != null && lastSaveID < 1) { lastSaveID++; }
#endif
return lastSaveID;
}
set { lastSaveID = value; }
set
{
#if SERVER
//trigger a campaign update to notify the clients of the changed save ID
lastUpdateID++;
#endif
lastSaveID = value;
}
}
public UInt16 PendingSaveID
{
get;
set;
}
private static byte currentCampaignID;
public byte CampaignID
{
get; private set;
get; set;
}
public MultiPlayerCampaign(GameModePreset preset, object param) :
base(preset, param)
private MultiPlayerCampaign() : base(GameModePreset.MultiPlayerCampaign)
{
currentCampaignID++;
CampaignID = currentCampaignID;
}
public override void Start()
{
base.Start();
if (GameMain.NetworkMember.IsServer) lastUpdateID++;
}
public override void End(string endMessage = "")
{
isRunning = false;
#if CLIENT
if (GameMain.Client != null)
{
bool success =
GameMain.Client.ConnectedClients.Any(c => c.Character != null && !c.Character.IsDead);
GameMain.GameSession.EndRound("");
GameMain.GameSession.CrewManager.EndRound();
if (success)
{
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
}
return;
}
#endif
#if SERVER
lastUpdateID++;
bool success =
GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
success = success || (GameMain.Server.Character != null && !GameMain.Server.Character.IsDead);
/*if (success)
{
if (subsToLeaveBehind == null || leavingSub == null)
{
DebugConsole.ThrowError("Leaving submarine not selected -> selecting the closest one");
leavingSub = GetLeavingSub();
subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub);
}
}*/
GameMain.GameSession.EndRound("");
//client character has spawned this round -> remove old data (and replace with an up-to-date one if the client still has an alive character)
characterData.RemoveAll(cd => cd.HasSpawned);
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.Character?.Info != null && !c.Character.IsDead)
{
c.Character.ResetCurrentOrder();
c.CharacterInfo = c.Character.Info;
characterData.Add(new CharacterCampaignData(c));
}
}
if (success)
{
bool atEndPosition = Submarine.MainSub.AtEndPosition;
/*if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub))
{
Submarine.MainSub = leavingSub;
GameMain.GameSession.Submarine = leavingSub;
foreach (Submarine sub in subsToLeaveBehind)
{
MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine);
LinkedSubmarine.CreateDummy(leavingSub, sub);
}
}*/
if (atEndPosition)
{
map.MoveToNextLocation();
//select a random location to make sure we've got some destination
//to head towards even if the host/clients don't select anything
map.SelectRandomLocation(true);
}
map.ProgressWorld();
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
}
#endif
CampaignMetadata = new CampaignMetadata(this);
UpgradeManager = new UpgradeManager(this);
InitCampaignData();
}
partial void SetDelegates();
public static MultiPlayerCampaign LoadNew(XElement element)
public static MultiPlayerCampaign StartNew(string mapSeed)
{
MultiPlayerCampaign campaign = new MultiPlayerCampaign(GameModePreset.List.Find(gm => gm.Identifier == "multiplayercampaign"), null);
campaign.Load(element);
campaign.SetDelegates();
MultiPlayerCampaign campaign = new MultiPlayerCampaign();
//only the server generates the map, the clients load it from a save file
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
campaign.map = new Map(campaign, mapSeed);
}
campaign.InitProjSpecific();
return campaign;
}
public static MultiPlayerCampaign LoadNew(XElement element)
{
MultiPlayerCampaign campaign = new MultiPlayerCampaign();
campaign.Load(element);
campaign.InitProjSpecific();
campaign.IsFirstRound = false;
return campaign;
}
partial void InitProjSpecific();
public static string GetCharacterDataSavePath(string savePath)
{
return Path.Combine(SaveUtil.MultiplayerSaveFolder, Path.GetFileNameWithoutExtension(savePath) + "_CharacterData.xml");
@@ -174,10 +90,12 @@ namespace Barotrauma
return GetCharacterDataSavePath(GameMain.GameSession.SavePath);
}
public void Load(XElement element)
/// <summary>
/// Loads the campaign from an XML element. Creates the map if it hasn't been created yet, otherwise updates the state of the map.
/// </summary>
private void Load(XElement element)
{
Money = element.GetAttributeInt("money", 0);
InitialSuppliesSpawned = element.GetAttributeBool("initialsuppliesspawned", false);
CheatsEnabled = element.GetAttributeBool("cheatsenabled", false);
if (CheatsEnabled)
{
@@ -195,6 +113,12 @@ namespace Barotrauma
#endif
}
#if SERVER
List<SubmarineInfo> availableSubs = new List<SubmarineInfo>();
List<SubmarineInfo> sourceList = new List<SubmarineInfo>();
sourceList.AddRange(SubmarineInfo.SavedSubmarines);
#endif
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -203,19 +127,55 @@ namespace Barotrauma
if (map == null)
{
//map not created yet, loading this campaign for the first time
map = Map.LoadNew(subElement);
map = Map.Load(this, subElement);
}
else
{
//map already created, update it
//if we're not downloading the initial save file (LastSaveID > 0),
//show notifications about location type changes
map.Load(subElement, LastSaveID > 0);
map.LoadState(subElement, LastSaveID > 0);
}
break;
case "metadata":
CampaignMetadata = new CampaignMetadata(this, subElement);
break;
case "pendingupgrades":
UpgradeManager = new UpgradeManager(this, subElement, isSingleplayer: false);
break;
case "bots" when GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer:
CrewManager.HasBots = subElement.GetAttributeBool("hasbots", false);
CrewManager.AddCharacterElements(subElement);
break;
case "cargo":
CargoManager?.LoadPurchasedItems(subElement);
break;
#if SERVER
case "availablesubs":
foreach (XElement availableSub in subElement.Elements())
{
string subName = availableSub.GetAttributeString("name", "");
SubmarineInfo matchingSub = sourceList.Find(s => s.Name == subName);
if (matchingSub != null) { availableSubs.Add(matchingSub); }
}
break;
#endif
}
}
CampaignMetadata ??= new CampaignMetadata(this);
UpgradeManager ??= new UpgradeManager(this);
InitCampaignData();
#if SERVER
// Fallback if using a save with no available subs assigned, use vanilla submarines
if (availableSubs.Count == 0)
{
GameMain.NetLobbyScreen.CampaignSubmarines.AddRange(sourceList.FindAll(s => s.IsCampaignCompatible && s.IsVanillaSubmarine()));
}
GameMain.NetLobbyScreen.CampaignSubmarines = availableSubs;
characterData.Clear();
string characterDataPath = GetCharacterDataSavePath();
var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath);
@@ -26,7 +26,10 @@ namespace Barotrauma
public Character.TeamType? WinningTeam;
public bool IsRunning { get; private set; }
public Level Level { get; private set; }
public LevelData LevelData { get; private set; }
public Map Map
{
@@ -36,17 +39,21 @@ namespace Barotrauma
}
}
public CampaignMode Campaign
{
get
{
return GameMode as CampaignMode;
}
}
public Location StartLocation
{
get
{
if (Map != null) return Map.CurrentLocation;
if (dummyLocations == null)
{
CreateDummyLocations();
}
if (Map != null) { return Map.CurrentLocation; }
if (dummyLocations == null) { CreateDummyLocations(); }
return dummyLocations[0];
}
}
@@ -55,18 +62,15 @@ namespace Barotrauma
{
get
{
if (Map != null) return Map.SelectedLocation;
if (dummyLocations == null)
{
CreateDummyLocations();
}
if (Map != null) { return Map.SelectedLocation; }
if (dummyLocations == null) { CreateDummyLocations(); }
return dummyLocations[1];
}
}
public SubmarineInfo SubmarineInfo { get; set; }
public List<SubmarineInfo> OwnedSubmarines = new List<SubmarineInfo>();
public Submarine Submarine { get; set; }
@@ -74,41 +78,55 @@ namespace Barotrauma
partial void InitProjSpecific();
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, MissionType missionType = MissionType.None)
: this(submarineInfo, savePath)
{
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = gameModePreset.Instantiate(missionType);
}
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, MissionPrefab missionPrefab)
: this(submarineInfo, savePath)
{
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = gameModePreset.Instantiate(missionPrefab);
#if CLIENT
if (GameMode is SubTestMode) { EventManager = null; }
#endif
}
private GameSession(SubmarineInfo submarineInfo, string savePath)
private GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines = null)
{
InitProjSpecific();
SubmarineInfo = submarineInfo;
/*Submarine = new Submarine(submarineInfo);
Submarine.MainSub = Submarine;*/
#if CLIENT
if (ownedSubmarines == null && GameMode is MultiPlayerCampaign && GameMain.NetLobbyScreen.ServerOwnedSubmarines != null)
{
ownedSubmarines = GameMain.NetLobbyScreen.ServerOwnedSubmarines;
}
#endif
OwnedSubmarines = ownedSubmarines ?? new List<SubmarineInfo>();
if (!OwnedSubmarines.Any(s => s.Name == submarineInfo.Name))
{
OwnedSubmarines.Add(submarineInfo);
}
GameMain.GameSession = this;
EventManager = new EventManager();
this.SavePath = savePath;
}
public GameSession(SubmarineInfo selectedSubInfo, string saveFile, XDocument doc)
: this(selectedSubInfo, saveFile)
/// <summary>
/// Start a new GameSession. Will be saved to the specified save path (if playing a game mode that can be saved).
/// </summary>
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, string seed = null, MissionType missionType = MissionType.None)
: this(submarineInfo)
{
Submarine.MainSub = Submarine;
this.SavePath = savePath;
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = InstantiateGameMode(gameModePreset, seed, missionType: missionType);
}
/// <summary>
/// Start a new GameSession with a specific pre-selected mission.
/// </summary>
public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, MissionPrefab missionPrefab = null)
: this(submarineInfo)
{
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = InstantiateGameMode(gameModePreset, seed, missionPrefab: missionPrefab);
}
/// <summary>
/// Load a game session from the specified XML document. The session will be saved to the specified path.
/// </summary>
public GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines, XDocument doc, string saveFile)
: this(submarineInfo, ownedSubmarines)
{
this.SavePath = saveFile;
GameMain.GameSession = this;
//selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name);
@@ -120,17 +138,62 @@ namespace Barotrauma
case "gamemode": //legacy support
case "singleplayercampaign":
CrewManager = new CrewManager(true);
GameMode = SinglePlayerCampaign.Load(subElement);
var campaign = SinglePlayerCampaign.Load(subElement);
campaign.LoadNewLevel();
GameMode = campaign;
break;
#endif
case "multiplayercampaign":
CrewManager = new CrewManager(false);
GameMode = MultiPlayerCampaign.LoadNew(subElement);
var mpCampaign = MultiPlayerCampaign.LoadNew(subElement);
GameMode = mpCampaign;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
//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;
}
}
}
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, MissionPrefab missionPrefab = null, MissionType missionType = MissionType.None)
{
if (gameModePreset.GameModeType == typeof(MissionMode))
{
return missionPrefab != null ?
new MissionMode(gameModePreset, missionPrefab) :
new MissionMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8));
}
else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
{
return MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8));
}
#if CLIENT
else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
{
return SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8));
}
else if (gameModePreset.GameModeType == typeof(TutorialMode))
{
return new TutorialMode(gameModePreset);
}
else if (gameModePreset.GameModeType == typeof(TestGameMode))
{
return new TestGameMode(gameModePreset);
}
#endif
else if (gameModePreset.GameModeType == typeof(GameMode))
{
return new GameMode(gameModePreset);
}
else
{
throw new Exception($"Could not find a game mode of the type \"{gameModePreset.GameModeType}\"");
}
}
private void CreateDummyLocations()
{
dummyLocations = new Location[2];
@@ -148,24 +211,146 @@ namespace Barotrauma
MTRandom rand = new MTRandom(ToolBox.StringToInt(seed));
for (int i = 0; i < 2; i++)
{
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand);
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true);
}
}
public void LoadPrevious()
public void LoadPreviousSave()
{
Submarine.Unload();
SaveUtil.LoadGame(SavePath);
}
public void StartRound(string levelSeed, float? difficulty = null)
/// <summary>
/// Switch to another submarine. The sub is loaded when the next round starts.
/// </summary>
public void SwitchSubmarine(SubmarineInfo newSubmarine, int cost)
{
Level randomLevel = Level.CreateRandom(levelSeed, difficulty);
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
OwnedSubmarines.Add(newSubmarine);
}
else
{
// Fetch owned submarine data as the newSubmarine is just the base submarine
for (int i = 0; i < OwnedSubmarines.Count; i++)
{
if (OwnedSubmarines[i].Name == newSubmarine.Name)
{
newSubmarine = OwnedSubmarines[i];
break;
}
}
}
StartRound(randomLevel);
Campaign.Money -= cost;
((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine;
}
public void StartRound(Level level, bool mirrorLevel = false)
public void PurchaseSubmarine(SubmarineInfo newSubmarine)
{
if (Campaign == null) return;
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
Campaign.Money -= newSubmarine.Price;
OwnedSubmarines.Add(newSubmarine);
}
}
public bool IsSubmarineOwned(SubmarineInfo query)
{
return
Submarine.MainSub.Info.Name == query.Name ||
(OwnedSubmarines != null && OwnedSubmarines.Any(os => os.Name == query.Name));
}
public void StartRound(string levelSeed, float? difficulty = null)
{
StartRound(LevelData.CreateRandom(levelSeed, difficulty));
}
public void StartRound(LevelData levelData, bool mirrorLevel = false, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
{
if (SubmarineInfo == null)
{
DebugConsole.ThrowError("Couldn't start game session, submarine not selected.");
return;
}
if (SubmarineInfo.IsFileCorrupted)
{
DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted.");
return;
}
LevelData = levelData;
if (GameMode is CampaignMode campaignMode && GameMode.Mission != null &&
LevelData != null && LevelData.Type == LevelData.LevelType.Outpost)
{
campaignMode.Map.CurrentLocation.SelectedMission = null;
}
Submarine.Unload();
Submarine = Submarine.MainSub = new Submarine(SubmarineInfo);
foreach (Submarine sub in Submarine.GetConnectedSubs())
{
sub.TeamID = Character.TeamType.Team1;
foreach (Item item in Item.ItemList)
{
if (item.Submarine != sub) { continue; }
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = sub.TeamID;
}
}
}
if (GameMode.Mission != null && GameMode.Mission.TeamCount > 1 && Submarine.MainSubs[1] == null)
{
Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true);
}
Level level = null;
if (levelData != null)
{
level = Level.Generate(levelData, mirrorLevel, startOutpost, endOutpost);
}
InitializeLevel(level);
GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Info.Name);
GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(levelData?.Seed ?? "[NO_LEVEL]"));
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start,
GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString()));
#if CLIENT
if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary;
if (existingRoundSummary?.ContinueButton != null)
{
existingRoundSummary.ContinueButton.Visible = true;
}
RoundSummary = new RoundSummary(Submarine.Info, GameMode, Mission, StartLocation, EndLocation);
if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode))
{
GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
if (EndLocation != null)
{
GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), (Mission == null ? TextManager.Get("None") : Mission.Name)), Color.CadetBlue, playSound: false);
}
else
{
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.Name), Color.CadetBlue, playSound: false);
}
}
#endif
}
private void InitializeLevel(Level level)
{
//make sure no status effects have been carried on from the next round
//(they should be stopped in EndRound, this is a safeguard against cases where the round is ended ungracefully)
@@ -175,83 +360,10 @@ namespace Barotrauma
GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null;
if (GameMain.Client == null) GameMain.LightManager.LosMode = GameMain.Config.LosMode;
#endif
this.Level = level;
LevelData = level?.LevelData;
Level = level;
if (SubmarineInfo == null)
{
DebugConsole.ThrowError("Couldn't start game session, submarine not selected.");
return;
}
if (SubmarineInfo.IsFileCorrupted)
{
DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted.");
return;
}
Submarine.Unload();
Submarine = Submarine.MainSub = new Submarine(SubmarineInfo);
Submarine.MainSub = Submarine;
if (GameMode.Mission != null && GameMode.Mission.TeamCount > 1 && Submarine.MainSubs[1] == null)
{
Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true);
}
if (level != null)
{
level.Generate(mirrorLevel);
if (level.StartOutpost != null)
{
//start by placing the sub below the outpost
Rectangle outpostBorders = Level.Loaded.StartOutpost.GetDockedBorders();
Rectangle subBorders = Submarine.GetDockedBorders();
Vector2 startOutpostSize = Vector2.Zero;
if (Level.Loaded.StartOutpost != null)
{
startOutpostSize = Level.Loaded.StartOutpost.Borders.Size.ToVector2();
}
Submarine.SetPosition(
Level.Loaded.StartOutpost.WorldPosition -
new Vector2(0.0f, outpostBorders.Height / 2 + subBorders.Height / 2));
//find the port that's the nearest to the outpost and dock if one is found
float closestDistance = 0.0f;
DockingPort myPort = null, outPostPort = null;
foreach (DockingPort port in DockingPort.List)
{
if (port.IsHorizontal || port.Docked) { continue; }
if (port.Item.Submarine == level.StartOutpost)
{
outPostPort = port;
continue;
}
if (port.Item.Submarine != Submarine) { continue; }
//the submarine port has to be at the top of the sub
if (port.Item.WorldPosition.Y < Submarine.WorldPosition.Y) { continue; }
float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition);
if ((myPort == null || dist < closestDistance || port.MainDockingPort) && !(myPort?.MainDockingPort ?? false))
{
myPort = port;
closestDistance = dist;
}
}
if (myPort != null && outPostPort != null)
{
Vector2 portDiff = myPort.Item.WorldPosition - Submarine.WorldPosition;
Submarine.SetPosition((outPostPort.Item.WorldPosition - portDiff) - Vector2.UnitY * outPostPort.DockedDistance);
myPort.Dock(outPostPort);
myPort.Lock(true);
}
}
else
{
Submarine.SetPosition(Submarine.FindSpawnPos(level.StartPosition));
}
}
PlaceSubAtStart(Level);
foreach (var sub in Submarine.Loaded)
{
@@ -267,9 +379,9 @@ namespace Barotrauma
if (GameMode != null) { GameMode.Start(); }
if (GameMode.Mission != null)
{
int prevEntityCount = Entity.GetEntityList().Count;
int prevEntityCount = Entity.GetEntities().Count();
Mission.Start(Level.Loaded);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntityList().Count != prevEntityCount)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount)
{
DebugConsole.ThrowError(
"Entity count has changed after starting a mission as a client. " +
@@ -278,7 +390,7 @@ namespace Barotrauma
}
}
EventManager?.StartRound(level);
EventManager?.StartRound(Level.Loaded);
SteamAchievementManager.OnStartRound();
if (GameMode != null)
@@ -289,37 +401,109 @@ namespace Barotrauma
{
//only place items and corpses here in single player
//the server does this after loading the respawn shuttle
Level?.SpawnNPCs();
Level?.SpawnCorpses();
AutoItemPlacer.PlaceIfNeeded(GameMode);
AutoItemPlacer.PlaceIfNeeded();
}
if (GameMode is MultiPlayerCampaign mpCampaign && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (GameMode is MultiPlayerCampaign mpCampaign)
{
mpCampaign.CargoManager.CreateItems();
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
mpCampaign.CargoManager.CreatePurchasedItems();
#if SERVER
mpCampaign.SendCrewState(false, null);
#endif
}
mpCampaign.UpgradeManager.ApplyUpgrades();
mpCampaign.UpgradeManager.SanityCheckUpgrades(Submarine);
}
if (GameMode is CampaignMode)
{
Submarine.WarmStartPower();
}
}
GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Info.Name);
GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(level?.Seed ?? "[NO_LEVEL]"));
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start,
GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString()));
#if CLIENT
if (GameMode is SinglePlayerCampaign) { SteamAchievementManager.OnBiomeDiscovered(level.Biome); }
if (!(GameMode is SubTestMode)) { RoundSummary = new RoundSummary(this); }
GameMain.GameScreen.ColorFade(Color.Black, Color.TransparentBlack, 5.0f);
if (!(GameMode is TutorialMode) && !(GameMode is SubTestMode))
{
GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
GUI.AddMessage(level.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, level.Difficulty / 100.0f), 5.0f, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), (Mission == null ? TextManager.Get("None") : Mission.Name)), Color.CadetBlue, playSound: false);
}
#endif
GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition;
RoundStartTime = Timing.TotalTime;
GameMain.ResetFrameTime();
IsRunning = true;
}
public void PlaceSubAtStart(Level level)
{
if (level == null)
{
Submarine.MainSub.SetPosition(Vector2.Zero);
return;
}
if (level.StartOutpost != null)
{
//start by placing the sub below the outpost
Rectangle outpostBorders = Level.Loaded.StartOutpost.GetDockedBorders();
Rectangle subBorders = Submarine.GetDockedBorders();
Submarine.SetPosition(
Level.Loaded.StartOutpost.WorldPosition -
new Vector2(0.0f, outpostBorders.Height / 2 + subBorders.Height / 2));
//find the port that's the nearest to the outpost and dock if one is found
float closestDistance = 0.0f;
DockingPort myPort = null, outPostPort = null;
foreach (DockingPort port in DockingPort.List)
{
if (port.IsHorizontal || port.Docked) { continue; }
if (port.Item.Submarine == level.StartOutpost)
{
outPostPort = port;
continue;
}
if (port.Item.Submarine != Submarine) { continue; }
//the submarine port has to be at the top of the sub
if (port.Item.WorldPosition.Y < Submarine.WorldPosition.Y) { continue; }
float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition);
if ((myPort == null || dist < closestDistance || port.MainDockingPort) && !(myPort?.MainDockingPort ?? false))
{
myPort = port;
closestDistance = dist;
}
}
if (myPort != null && outPostPort != null)
{
Vector2 portDiff = myPort.Item.WorldPosition - Submarine.WorldPosition;
Vector2 spawnPos = (outPostPort.Item.WorldPosition - portDiff) - Vector2.UnitY * outPostPort.DockedDistance;
bool startDocked = level.Type == LevelData.LevelType.Outpost;
#if CLIENT
startDocked |= GameMode is TutorialMode;
#endif
if (startDocked)
{
Submarine.SetPosition(spawnPos);
myPort.Dock(outPostPort);
myPort.Lock(true);
}
else
{
Submarine.SetPosition(spawnPos - Vector2.UnitY * 100.0f);
Submarine.NeutralizeBallast();
Submarine.EnableMaintainPosition();
}
}
else
{
Submarine.NeutralizeBallast();
Submarine.EnableMaintainPosition();
}
}
else
{
Submarine.SetPosition(Submarine.FindSpawnPos(level.StartPosition, verticalMoveDir: 1));
Submarine.NeutralizeBallast();
Submarine.EnableMaintainPosition();
}
}
public void Update(float deltaTime)
@@ -333,35 +517,35 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime);
public void EndRound(string endMessage)
public void EndRound(string endMessage, List<TraitorMissionResult> traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
{
if (Mission != null) Mission.End();
if (Mission != null) { Mission.End(); }
GameAnalyticsManager.AddProgressionEvent(
(Mission == null || Mission.Completed) ? GameAnalyticsSDK.Net.EGAProgressionStatus.Complete : GameAnalyticsSDK.Net.EGAProgressionStatus.Fail,
(Mission == null || Mission.Completed) ? GameAnalyticsSDK.Net.EGAProgressionStatus.Complete : GameAnalyticsSDK.Net.EGAProgressionStatus.Fail,
GameMode.Preset.Identifier,
(Mission == null ? "None" : Mission.GetType().ToString()));
Mission == null ? "None" : Mission.GetType().ToString());
#if CLIENT
if (RoundSummary != null)
if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null)
{
GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(endMessage);
GUI.ClearMessages();
GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary);
GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, traitorResults, transitionType);
GUIMessageBox.MessageBoxes.Add(summaryFrame);
var okButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), summaryFrame.Children.First().Children.First().FindChild("buttonarea").RectTransform),
TextManager.Get("OK"))
{
OnClicked = (GUIButton button, object obj) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }
};
RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; };
}
if (GameMain.NetLobbyScreen != null) GameMain.NetLobbyScreen.OnRoundEnded();
TabMenu.OnRoundEnded();
GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction");
#endif
EventManager?.EndRound();
SteamAchievementManager.OnRoundEnded(this);
Mission = null;
GameMode?.End(transitionType);
EventManager?.EndRound();
StatusEffect.StopAll();
Mission = null;
IsRunning = false;
}
public void KillCharacter(Character character)
@@ -455,7 +639,18 @@ namespace Barotrauma
XDocument doc = new XDocument(new XElement("Gamesession"));
doc.Root.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal));
doc.Root.Add(new XAttribute("version", GameMain.Version));
doc.Root.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name));
if (OwnedSubmarines != null)
{
List<string> ownedSubmarineNames = new List<string>();
var ownedSubsElement = new XElement("ownedsubmarines");
doc.Root.Add(ownedSubsElement);
foreach (var ownedSub in OwnedSubmarines)
{
ownedSubsElement.Add(new XElement("sub", new XAttribute("name", ownedSub.Name)));
}
}
doc.Root.Add(new XAttribute("mapseed", Map.Seed));
doc.Root.Add(new XAttribute("selectedcontentpackages",
string.Join("|", GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path))));
@@ -472,7 +667,7 @@ namespace Barotrauma
}
}
public void Load(XElement saveElement)
/*public void Load(XElement saveElement)
{
foreach (XElement subElement in saveElement.Elements())
{
@@ -495,7 +690,7 @@ namespace Barotrauma
break;
}
}
}
}*/
}
}
@@ -1,45 +1,43 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace Barotrauma
{
class HireManager
{
private List<CharacterInfo> availableCharacters;
public IEnumerable<CharacterInfo> AvailableCharacters
{
get { return availableCharacters; }
}
public List<CharacterInfo> AvailableCharacters { get; set; }
public List<CharacterInfo> PendingHires = new List<CharacterInfo>();
public const int MaxAvailableCharacters = 10;
public HireManager()
{
availableCharacters = new List<CharacterInfo>();
AvailableCharacters = new List<CharacterInfo>();
}
public void RemoveCharacter(CharacterInfo character)
{
availableCharacters.Remove(character);
AvailableCharacters.Remove(character);
}
public void GenerateCharacters(Location location, int amount)
{
availableCharacters.ForEach(c => c.Remove());
availableCharacters.Clear();
AvailableCharacters.ForEach(c => c.Remove());
AvailableCharacters.Clear();
for (int i = 0; i < amount; i++)
{
JobPrefab job = location.Type.GetRandomHireable();
if (job == null) { return; }
var variant = Rand.Range(0, job.Variants, Rand.RandSync.Server);
availableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, variant: variant));
AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, variant: variant));
}
}
public void Remove()
{
availableCharacters.ForEach(c => c.Remove());
availableCharacters.Clear();
AvailableCharacters.ForEach(c => c.Remove());
AvailableCharacters.Clear();
}
}
}
@@ -0,0 +1,747 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal class PurchasedUpgrade
{
public readonly UpgradeCategory Category;
public readonly UpgradePrefab Prefab;
public int Level;
public PurchasedUpgrade(UpgradePrefab upgradePrefab, UpgradeCategory category, int level = 1)
{
Category = category;
Prefab = upgradePrefab;
Level = level;
}
public void Deconstruct(out UpgradePrefab prefab, out UpgradeCategory category, out int level)
{
prefab = Prefab;
category = Category;
level = Level;
}
}
/// <summary>
/// This class handles all upgrade logic.
/// Storing, applying, checking and validation of upgrades.
/// </summary>
/// <remarks>
/// Upgrades are applied per item basis meaning each item has their own set of slots for upgrades.
/// The store applies upgrades globally to categories of items so the purpose of this class is to keep those individual "upgrade slots" in sync.
/// The target level of an upgrade is stored in the metadata and is what the store displays and modifies while this class will make sure that
/// the upgrades on the items match the values stored in the metadata.
/// </remarks>
partial class UpgradeManager
{
/// <summary>
/// This one toggles whether or not connected submarines get upgraded too.
/// Could probably be removed, I just didn't like magic numbers.
/// </summary>
public const bool UpgradeAlsoConnectedSubs = true;
/// <summary>
/// Prevents the player from upgrading the submarine when we are switching to a new one.
/// </summary>
/// <remarks>
/// In singleplayer we check if CampaignMode.PendingSubmarineSwitch is not null indicating we are switching submarines
/// but in multiplayer that value is not synced so we use this variable instead by setting it to false in <see cref="UpgradeManager.ClientRead"/>
/// and then set it back to true when the round ends in <see cref="MultiPlayerCampaign.End"/>
/// </remarks>
public bool CanUpgrade = true;
/// <summary>
/// This is used by the client in multiplayer, acts like a secondary PendingUpgrades list
/// but is not affected by server messages.
/// </summary>
/// <remarks>
/// Not used in singleplayer.
/// </remarks>
private List<PurchasedUpgrade>? loadedUpgrades;
/// <summary>
/// This is used by the client to notify the server which upgrades are yet to be paid for.
/// </summary>
/// <remarks>
/// In singleplayer this does nothing.
/// </remarks>
public readonly List<PurchasedUpgrade> PurchasedUpgrades = new List<PurchasedUpgrade>();
public readonly List<PurchasedUpgrade> PendingUpgrades = new List<PurchasedUpgrade>();
private CampaignMetadata Metadata => Campaign.CampaignMetadata;
private readonly CampaignMode Campaign;
private int spentMoney;
public event Action? OnUpgradesChanged;
public UpgradeManager(CampaignMode campaign)
{
DebugConsole.Log("Created brand new upgrade manager.");
Campaign = campaign;
}
public UpgradeManager(CampaignMode campaign, XElement element, bool isSingleplayer) : this(campaign)
{
DebugConsole.Log($"Restored upgrade manager from save file, ({element.Elements().Count()} pending upgrades).");
LoadPendingUpgrades(element, isSingleplayer);
}
private DateTime lastUpgradeSpeak, lastErrorSpeak;
/// <summary>
/// Purchases an upgrade and handles logic for deducting the credit.
/// </summary>
/// <remarks>
/// Purchased upgrades are temporarily stored in <see cref="PendingUpgrades"/> and they are applied
/// after the next round starts similarly how items are spawned in the stowage room after the round starts.
/// </remarks>
/// <param name="prefab"></param>
/// <param name="category"></param>
public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category)
{
if (!CanUpgradeSub())
{
DebugConsole.ThrowError("Cannot upgrade when switching to another submarine.");
return;
}
int price = prefab.Price.GetBuyprice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation);
int currentLevel = GetUpgradeLevel(prefab, category);
if (currentLevel + 1 > prefab.MaxLevel)
{
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {prefab.MaxLevel}). The transaction has been cancelled.");
return;
}
if (price < 0)
{
Location? location = Campaign.Map?.CurrentLocation;
LogError($"Upgrade price is less than 0! ({price})",
new Dictionary<string, object?>
{
{ "Level", currentLevel },
{ "Saved Level", GetRealUpgradeLevel(prefab, category) },
{ "Upgrade", $"{category.Identifier}.{prefab.Identifier}" },
{ "Location", location?.Type },
{ "Reputation", $"{location?.Reputation?.Value} / {location?.Reputation?.MaxReputation}" },
{ "Base Price", prefab.Price.BasePrice }
});
}
if (Campaign.Money > price)
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
// only make the NPC speak if more than 5 minutes have passed since the last purchased service
if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
{
UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer);
lastUpgradeSpeak = DateTime.Now;
}
}
Campaign.Money -= price;
spentMoney += price;
PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
#if CLIENT
DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for ${price}", GUI.Style.Orange);
#endif
if (upgrade == null)
{
PendingUpgrades.Add(new PurchasedUpgrade(prefab, category));
}
else
{
upgrade.Level++;
}
#if CLIENT
// tell the server that this item is yet to be paid for server side
PurchasedUpgrades.Add(new PurchasedUpgrade(prefab, category));
#endif
OnUpgradesChanged?.Invoke();
}
else
{
DebugConsole.ThrowError("Tried to purchase an upgrade with insufficient funds, the transaction has not been completed.\n" +
$"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.Money}");
}
}
/// <summary>
/// Applies all our pending upgrades to the submarine.
/// </summary>
/// <remarks>
/// Upgrades are applied similarly to how items on the submarine are spawned at the start of the round.
/// Upgrades should be applied at the start of the round and after the round ends they are written into
/// the submarine save and saved there.
/// Because of the difficulty of accessing the actual Submarine object from and outpost or when the campaign UI is created
/// we modify levels that are shown on the store interface using campaign metadata.
///
/// This method should be called by both the client and the server during level generation.
/// <see cref="SetUpgradeLevel"/>
/// <seealso cref="GetUpgradeLevel"/>
/// </remarks>
public void ApplyUpgrades()
{
PurchasedUpgrades.Clear();
if (Submarine.MainSub == null) { return; }
List<PurchasedUpgrade> pendingUpgrades = PendingUpgrades;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
if (Level.Loaded?.Type != LevelData.LevelType.Outpost)
{
if (loadedUpgrades != null)
{
// client receives pending upgrades from the save file
pendingUpgrades = loadedUpgrades;
}
}
else
{
// prevent the client from applying pending upgrades at an outpost when joining mid round
return;
}
}
DebugConsole.Log("Applying upgrades...");
foreach (var (prefab, category, level) in pendingUpgrades)
{
int newLevel = BuyUpgrade(prefab, category, Submarine.MainSub, level);
DebugConsole.Log($" - {category.Identifier}.{prefab.Identifier} lvl. {level}, new: ({newLevel})");
if (newLevel > 0)
{
SetUpgradeLevel(prefab, category, Math.Clamp(newLevel, 0, prefab.MaxLevel));
}
}
PendingUpgrades.Clear();
loadedUpgrades?.Clear();
loadedUpgrades = null;
spentMoney = 0;
}
/// <summary>
/// Cancels the pending upgrades and refunds the money spent
/// </summary>
private void RefundUpgrades()
{
DebugConsole.Log($"Refunded {spentMoney} marks in pending upgrades.");
if (spentMoney > 0)
{
#if CLIENT
GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("UpgradeRefundTitle"), TextManager.Get("UpgradeRefundBody"), new[] { TextManager.Get("Ok") });
msgBox.Buttons[0].OnClicked += msgBox.Close;
#endif
}
Campaign.Money += spentMoney;
spentMoney = 0;
PendingUpgrades.Clear();
PurchasedUpgrades.Clear();
}
public void CreateUpgradeErrorMessage(string text, bool isSinglePlayer, Character character)
{
// 10 second cooldown on the error message but not the UI sound
if (lastErrorSpeak == DateTime.MinValue || lastErrorSpeak.AddSeconds(10) < DateTime.Now)
{
UpgradeNPCSpeak(text, isSinglePlayer, character);
lastErrorSpeak = DateTime.Now;
}
#if CLIENT
GUI.PlayUISound(GUISoundType.PickItemFail);
#endif
}
/// <summary>
/// Makes the NPC talk or if no NPC has been specified find the upgrade NPC and make it talk.
/// </summary>
/// <param name="text"></param>
/// <param name="isSinglePlayer"></param>
/// <param name="character">Optional NPC to make talk, if null tries to find one at the outpost.</param>
/// <remarks>
/// This might seem a bit spaghetti but it's the only way I could figure out how to do this and make it work
/// in both multiplayer and singleplayer because in multiplayer the client doesn't have access to SubmarineInfo.OutpostNPCs list
/// so we cannot find the upgrade NPC using that and the client cannot use Character.Speak anyways in multiplayer so the alternative
/// is to send network packages when interacting with the NPC.
/// </remarks>
partial void UpgradeNPCSpeak(string text, bool isSinglePlayer, Character? character = null);
/// <summary>
/// Validates that upgrade values stored in CampaignMetadata matches the values on the submarine and fixes any inconsistencies.
/// Should be called after every round start right after <see cref="ApplyUpgrades"/>
/// </summary>
/// <param name="submarine"></param>
public void SanityCheckUpgrades(Submarine submarine)
{
// check walls
foreach (Structure wall in submarine.GetWalls(UpgradeAlsoConnectedSubs))
{
foreach (UpgradeCategory category in UpgradeCategory.Categories)
{
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
{
int level = GetRealUpgradeLevel(prefab, category);
if (level == 0 || !prefab.IsWallUpgrade) { continue; }
Upgrade? upgrade = wall.GetUpgrade(prefab.Identifier);
bool isOverMax = IsOverMaxLevel(level, prefab);
if (isOverMax)
{
SetUpgradeLevel(prefab, category, prefab.MaxLevel);
level = prefab.MaxLevel;
}
if (upgrade == null || upgrade.Level != level || isOverMax)
{
DebugConsole.AddWarning($"{wall.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing...");
FixUpgradeOnItem(wall, prefab, level);
}
}
}
}
// Check items
foreach (Item item in submarine.GetItems(UpgradeAlsoConnectedSubs))
{
foreach (UpgradeCategory category in UpgradeCategory.Categories)
{
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
{
if (!category.CanBeApplied(item, prefab)) { continue; }
int level = GetRealUpgradeLevel(prefab, category);
if (level == 0) { continue; }
Upgrade? upgrade = item.GetUpgrade(prefab.Identifier);
bool isOverMax = IsOverMaxLevel(level, prefab);
if (isOverMax)
{
SetUpgradeLevel(prefab, category, prefab.MaxLevel);
level = prefab.MaxLevel;
}
if (upgrade == null || upgrade.Level != level || isOverMax)
{
DebugConsole.AddWarning($"{item.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing...");
FixUpgradeOnItem(item, prefab, level);
}
}
}
}
static bool IsOverMaxLevel(int level, UpgradePrefab prefab) => level > prefab.MaxLevel;
}
private static void FixUpgradeOnItem(ISerializableEntity target, UpgradePrefab prefab, int level)
{
if (target is MapEntity mapEntity)
{
mapEntity.SetUpgrade(new Upgrade(target, prefab, level), false);
}
}
/// <summary>
/// Applies an upgrade on the submarine, should be called by <see cref="ApplyUpgrades"/> when the round starts.
/// </summary>
/// <param name="prefab"></param>
/// <param name="category"></param>
/// <param name="submarine"></param>
/// <param name="level"></param>
/// <returns>New level that was applied, -1 if no upgrades were applied.</returns>
private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1)
{
int? newLevel = null;
if (category.IsWallUpgrade)
{
foreach (Structure structure in submarine.GetWalls(UpgradeAlsoConnectedSubs))
{
Upgrade upgrade = new Upgrade(structure, prefab, level);
structure.AddUpgrade(upgrade, createNetworkEvent: false);
Upgrade? newUpgrade = structure.GetUpgrade(prefab.Identifier);
if (newUpgrade != null)
{
SanityCheck(newUpgrade, structure);
newLevel ??= newUpgrade.Level;
}
}
}
else
{
foreach (Item item in submarine.GetItems(UpgradeAlsoConnectedSubs))
{
if (category.CanBeApplied(item, prefab))
{
Upgrade upgrade = new Upgrade(item, prefab, level);
item.AddUpgrade(upgrade, createNetworkEvent: false);
Upgrade? newUpgrade = item.GetUpgrade(prefab.Identifier);
if (newUpgrade != null)
{
SanityCheck(newUpgrade, item);
newLevel ??= newUpgrade.Level;
}
}
}
}
foreach (Submarine loadedSub in Submarine.Loaded.Where(sub => sub != submarine))
{
XElement? root = loadedSub.Info?.SubmarineElement;
if (root == null) { continue; }
if (root.Name.ToString().Equals("LinkedSubmarine", StringComparison.OrdinalIgnoreCase))
{
if (root.Attribute("location") == null) { continue; }
// Check if this is our linked submarine
ushort dockingPortID = (ushort) root.GetAttributeInt("originallinkedto", 0);
if (dockingPortID > 0 && submarine.GetItems(true).Any(item => item.ID == dockingPortID))
{
BuyUpgrade(prefab, category, loadedSub, level);
}
}
}
return newLevel ?? -1;
void SanityCheck(Upgrade newUpgrade, MapEntity target)
{
if (newLevel != null && newLevel != newUpgrade.Level)
{
// automatically fix this if it ever happens?
DebugConsole.AddWarning($"The upgrade {newUpgrade.Prefab.Name} in {target.Name} has a different level compared to other items! \n" +
$"Expected level was ${newLevel} but got {newUpgrade.Level} instead.");
}
}
}
/// <summary>
/// Gets the progress that is shown on the store interface.
/// Includes values stored in the metadata and <see cref="PendingUpgrades"/>
/// </summary>
/// <param name="prefab"></param>
/// <param name="category"></param>
/// <returns></returns>
public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category)
{
if (!Metadata.HasKey(FormatIdentifier(prefab, category))) { return GetPendingLevel(); }
return GetRealUpgradeLevel(prefab, category) + GetPendingLevel();
int GetPendingLevel()
{
PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
return upgrade?.Level ?? 0;
}
}
/// <summary>
/// Gets the level of the upgrade that is stored in the metadata.
/// </summary>
/// <param name="prefab"></param>
/// <param name="category"></param>
/// <returns></returns>
public int GetRealUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category)
{
return !Metadata.HasKey(FormatIdentifier(prefab, category)) ? 0 : Metadata.GetInt(FormatIdentifier(prefab, category), 0);
}
/// <summary>
/// Stores the target upgrade level in the campaign metadata.
/// </summary>
/// <param name="prefab"></param>
/// <param name="category"></param>
/// <param name="level"></param>
private void SetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, int level)
{
Metadata.SetValue(FormatIdentifier(prefab, category), level);
}
public bool CanUpgradeSub()
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return CanUpgrade; }
return Campaign.PendingSubmarineSwitch == null;
}
public void RefundResetAndReload(SubmarineInfo newSubmarine, bool notifyClients = false)
{
RefundUpgrades();
ResetUpgrades();
Dictionary<string, int> newUpgrades = ReloadUpgradeValues(newSubmarine);
#if SERVER
if (notifyClients)
{
SendUpgradeResetMessage(newUpgrades);
}
#endif
}
/// <summary>
/// Parses a SubmarineInfo and sets the store values accordingly.
/// Used when reloading a previously saved submarine.
/// </summary>
/// <param name="info"></param>
private Dictionary<string, int> ReloadUpgradeValues(SubmarineInfo info)
{
Dictionary<string, int> newValues = new Dictionary<string, int>();
IEnumerable<XElement> linkedSubElements = info.SubmarineElement.Elements().Where(element => element.Name.ToString().Equals("LinkedSubmarine", StringComparison.OrdinalIgnoreCase)).SelectMany(element => element.Elements());
IEnumerable<XElement> mainSubElements = info.SubmarineElement.Elements().Where(Predicate);
List<XElement> elements = mainSubElements.Concat(linkedSubElements.Where(Predicate)).ToList();
foreach (UpgradeCategory category in UpgradeCategory.Categories)
{
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
{
if (!prefab.UpgradeCategories.Contains(category)) { continue; }
List<int> levels = GetUpgradeFromXML(elements, category, prefab);
if (levels.Any())
{
int level = (int) levels.Average(i => i);
newValues.Add(FormatIdentifier(prefab, category), level);
}
}
}
foreach (var (dataIdentifier, level) in newValues)
{
Campaign.CampaignMetadata.SetValue(dataIdentifier, level);
}
return newValues;
static List<int> GetUpgradeFromXML(List<XElement> elements, UpgradeCategory category, UpgradePrefab prefab)
{
List<int> levels = new List<int>();
foreach (XElement subElement in elements)
{
if (!category.CanBeApplied(subElement)) { continue; }
foreach (XElement component in subElement.Elements())
{
if (string.Equals(component.Name.ToString(), "upgrade", StringComparison.OrdinalIgnoreCase))
{
string identifier = component.GetAttributeString("identifier", string.Empty);
int level = component.GetAttributeInt("level", -1);
if (string.IsNullOrWhiteSpace(identifier) || level <= 0) { continue; }
UpgradePrefab? matchingPrefab = UpgradePrefab.Find(identifier);
if (matchingPrefab == null || matchingPrefab != prefab) { continue; }
if (matchingPrefab.UpgradeCategories.Contains(category)) { levels.Add(level); }
}
}
}
return levels;
}
static bool Predicate(XElement element) => element.HasElements && element.Elements().Any(e => e.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Resets our upgrade progress and prices.
/// This does not actually remove the upgrades from the submarine but resets the store interface.
/// </summary>
/// <remarks>
/// This method works by iterating thru all upgrade categories and prefabs and checking if they have a
/// valid key stored in the metadata, if they do set it to 0, upgrades without a key stored are always
/// assumed to be 0 so they don't need to be reset.
///
/// Should initially be called server side as we can't trust clients with such a simple notification.
/// </remarks>
private void ResetUpgrades()
{
foreach (UpgradeCategory category in UpgradeCategory.Categories)
{
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
{
if (!prefab.UpgradeCategories.Contains(category)) { continue; }
string dataIdentifier = FormatIdentifier(prefab, category);
if (Metadata.HasKey(dataIdentifier))
{
Metadata.SetValue(dataIdentifier, 0);
}
}
}
OnUpgradesChanged?.Invoke();
}
public void SavePendingUpgrades(XElement? parent, List<PurchasedUpgrade> upgrades)
{
if (parent == null) { return; }
DebugConsole.Log("Saving pending upgrades to save file...");
XElement upgradeElement = new XElement("PendingUpgrades");
foreach (var (prefab, category, level) in upgrades)
{
upgradeElement.Add(new XElement("PendingUpgrade",
new XAttribute("category", category.Identifier),
new XAttribute("prefab", prefab.Identifier),
new XAttribute("level", level)));
}
DebugConsole.Log($"Saved {upgradeElement.Elements().Count()} pending upgrades.");
parent.Add(upgradeElement);
}
private void LoadPendingUpgrades(XElement? element, bool isSingleplayer = true)
{
if (element == null || !element.HasElements) { return; }
List<PurchasedUpgrade> pendingUpgrades = new List<PurchasedUpgrade>();
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (XElement upgrade in element.Elements())
{
string? categoryIdentifier = upgrade.GetAttributeString("category", null);
UpgradeCategory? category = UpgradeCategory.Find(categoryIdentifier);
if (string.IsNullOrWhiteSpace(categoryIdentifier) || category == null) { continue; }
string? prefabIdentifier = upgrade.GetAttributeString("prefab", null);
UpgradePrefab? prefab = UpgradePrefab.Find(prefabIdentifier);
if (string.IsNullOrWhiteSpace(prefabIdentifier) || prefab == null) { continue; }
int level = upgrade.GetAttributeInt("level", -1);
if (level < 0) { continue; }
pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, level));
}
if (isSingleplayer)
{
SetPendingUpgrades(pendingUpgrades);
}
else
{
loadedUpgrades = pendingUpgrades;
}
}
public static void LogError(string text, Dictionary<string, object?> data, Exception e = null)
{
string error = $"{text}\n";
foreach (var (label, value) in data)
{
error += $" - {label}: {value ?? "NULL"}\n";
}
DebugConsole.ThrowError(error.TrimEnd('\n'), e);
}
public static Dictionary<string, int> GetMetadataLevels(CampaignMetadata? metadata)
{
Dictionary<string, int> values = new Dictionary<string, int>();
if (metadata == null) { return values; }
foreach (UpgradeCategory category in UpgradeCategory.Categories)
{
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
{
string identifier = FormatIdentifier(prefab, category);
if (metadata.HasKey(identifier) && !values.ContainsKey(identifier))
{
values.Add(identifier, metadata.GetInt(identifier));
}
}
}
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>
/// <param name="upgrades"></param>
/// <remarks>
/// In singleplayer this is not used and should not be.
/// </remarks>
public void SetPendingUpgrades(List<PurchasedUpgrade> upgrades)
{
PendingUpgrades.Clear();
PendingUpgrades.AddRange(upgrades);
OnUpgradesChanged?.Invoke();
}
public static void DebugLog(string msg, Color? color = null)
{
#if UNSTABLE || DEBUG
DebugConsole.NewMessage(msg, color ?? Color.GreenYellow);
#else
DebugConsole.Log(msg);
#endif
}
private PurchasedUpgrade? FindMatchingUpgrade(UpgradePrefab prefab, UpgradeCategory category) => PendingUpgrades.Find(u => u.Prefab == prefab && u.Category == category);
private static string FormatIdentifier(UpgradePrefab prefab, UpgradeCategory category) => $"upgrade.{category.Identifier}.{prefab.Identifier}";
}
}
@@ -51,8 +51,11 @@ namespace Barotrauma
public bool VoipAttenuationEnabled { get; set; }
public bool UseDirectionalVoiceChat { get; set; }
public IList<string> AudioDeviceNames;
public IList<string> CaptureDeviceNames;
public string AudioOutputDevice { get; set; }
public enum VoiceMode
{
Disabled,
@@ -266,6 +269,7 @@ namespace Barotrauma
#if DEBUG
public bool AutomaticQuickStartEnabled { get; set; }
public bool AutomaticCampaignLoadEnabled { get; set; }
public bool TextManagerDebugModeEnabled { get; set; }
#endif
@@ -288,6 +292,8 @@ namespace Barotrauma
public void SelectCorePackage(ContentPackage contentPackage, bool forceReloadAll = false)
{
if (!contentPackage.ContainsRequiredCorePackageFiles(out _)) { return; }
ContentPackage otherCorePackage = SelectedContentPackages.Where(cp => cp.CorePackage).First();
SelectedContentPackages.Remove(otherCorePackage);
@@ -304,12 +310,19 @@ namespace Barotrauma
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
DisableContentPackageItems(filesToRemove.OrderBy(ContentFileLoadOrder));
EnableContentPackageItems(filesToAdd.OrderBy(ContentFileLoadOrder));
RefreshContentPackageItems(filesToAdd.Concat(filesToRemove));
}
public void AutoSelectCorePackage(IEnumerable<ContentPackage> toRemove)
{
SelectCorePackage(ContentPackage.List.Find(cpp =>
cpp.CorePackage &&
!toRemove.Contains(cpp) &&
cpp.ContainsRequiredCorePackageFiles(out _)));
}
public void SelectContentPackage(ContentPackage contentPackage)
{
if (!SelectedContentPackages.Contains(contentPackage))
@@ -318,7 +331,6 @@ namespace Barotrauma
ContentPackage.SortContentPackages();
EnableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder));
RefreshContentPackageItems(contentPackage.Files);
}
}
@@ -329,14 +341,11 @@ namespace Barotrauma
{
SelectedContentPackages.Remove(contentPackage);
ContentPackage.SortContentPackages();
DisableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder));
RefreshContentPackageItems(contentPackage.Files);
}
}
private void EnableContentPackageItems(IOrderedEnumerable<ContentFile> files)
{
foreach (ContentFile file in files)
@@ -427,14 +436,20 @@ namespace Barotrauma
private void RefreshContentPackageItems(IEnumerable<ContentFile> files)
{
if (files.Any(f => f.Type == ContentType.LocationTypes)) { LocationType.Init(); }
if (files.Any(f => f.Type == ContentType.Afflictions)) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); }
if (files.Any(f => f.Type == ContentType.Submarine)) { SubmarineInfo.RefreshSavedSubs(); }
if (files.Any(f => f.Type == ContentType.Submarine ||
f.Type == ContentType.Outpost ||
f.Type == ContentType.OutpostModule ||
f.Type == ContentType.Wreck)) { SubmarineInfo.RefreshSavedSubs(); }
if (files.Any(f => f.Type == ContentType.NPCSets)) { NPCSet.LoadSets(); }
if (files.Any(f => f.Type == ContentType.OutpostConfig)) { OutpostGenerationParams.LoadPresets(); }
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)) { ScriptedEventSet.LoadPrefabs(); }
if (files.Any(f => f.Type == ContentType.RandomEvents)) { EventSet.LoadPrefabs(); }
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.LocationTypes)) { LocationType.Init(); }
if (files.Any(f => f.Type == ContentType.MapGenerationParameters)) { MapGenerationParams.Init(); }
if (files.Any(f => f.Type == ContentType.LevelGenerationParameters)) { LevelGenerationParams.LoadPresets(); }
if (files.Any(f => f.Type == ContentType.TraitorMissions)) { TraitorMissionPrefab.Init(); }
@@ -474,6 +489,10 @@ namespace Barotrauma
ContentType.Particles,
ContentType.Decals,
ContentType.Outpost,
ContentType.OutpostModule,
ContentType.OutpostConfig,
ContentType.NPCSets,
ContentType.Factions,
ContentType.Wreck,
ContentType.WreckAIConfig,
ContentType.BackgroundCreaturePrefabs,
@@ -516,12 +535,13 @@ namespace Barotrauma
SubmarineInfo.RefreshSavedSubs();
ItemPrefab.InitFabricationRecipes();
RuinGeneration.RuinGenerationParams.ClearAll();
ScriptedEventSet.LoadPrefabs();
EventSet.LoadPrefabs();
MissionPrefab.Init();
LevelObjectPrefab.LoadAll();
LocationType.Init();
MapGenerationParams.Init();
LevelGenerationParams.LoadPresets();
OutpostGenerationParams.LoadPresets();
TraitorMissionPrefab.Init();
Order.Init();
EventManagerSettings.Init();
@@ -658,7 +678,7 @@ namespace Barotrauma
{
if (cp.CorePackage)
{
GameMain.Config.SelectCorePackage(ContentPackage.List.Find(cpp => cpp.CorePackage && !toRemove.Contains(cpp)));
GameMain.Config.AutoSelectCorePackage(toRemove);
}
else
{
@@ -1105,6 +1125,7 @@ namespace Barotrauma
new XAttribute("usedualmodesockets", UseDualModeSockets)
#if DEBUG
, new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled)
, new XAttribute("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled)
, new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled)
#endif
);
@@ -1162,6 +1183,7 @@ namespace Barotrauma
new XAttribute("voipattenuationenabled", VoipAttenuationEnabled),
new XAttribute("usedirectionalvoicechat", UseDirectionalVoiceChat),
new XAttribute("voicesetting", VoiceSetting),
new XAttribute("audiooutputdevice", System.Xml.XmlConvert.EncodeName(AudioOutputDevice ?? "")),
new XAttribute("voicecapturedevice", System.Xml.XmlConvert.EncodeName(VoiceCaptureDevice ?? "")),
new XAttribute("noisegatethreshold", NoiseGateThreshold));
@@ -1313,6 +1335,7 @@ namespace Barotrauma
UseDualModeSockets = doc.Root.GetAttributeBool("usedualmodesockets", true);
#if DEBUG
AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled);
AutomaticCampaignLoadEnabled = doc.Root.GetAttributeBool("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled);
TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled);
#endif
XElement gameplayElement = doc.Root.Element("gameplay");
@@ -1405,6 +1428,7 @@ namespace Barotrauma
UseDirectionalVoiceChat = audioSettings.GetAttributeBool("usedirectionalvoicechat", UseDirectionalVoiceChat);
VoiceCaptureDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice));
AudioOutputDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("audiooutputdevice", AudioOutputDevice));
NoiseGateThreshold = audioSettings.GetAttributeFloat("noisegatethreshold", NoiseGateThreshold);
MicrophoneVolume = audioSettings.GetAttributeFloat("microphonevolume", MicrophoneVolume);
string voiceSettingStr = audioSettings.GetAttributeString("voicesetting", "");
@@ -25,9 +25,6 @@ namespace Barotrauma.Items.Components
private readonly Hull[] hulls = new Hull[2];
private Gap gap;
private Door door;
private Body[] bodies;
private Fixture outsideBlocker;
private Body doorBody;
@@ -68,6 +65,8 @@ namespace Barotrauma.Items.Components
public DockingPort DockingTarget { get; private set; }
public Door Door { get; private set; }
public bool Docked
{
get
@@ -208,7 +207,7 @@ namespace Barotrauma.Items.Components
CreateJoint(false);
#if SERVER
if (GameMain.Server != null)
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
item.CreateServerEvent(this);
}
@@ -250,7 +249,7 @@ namespace Barotrauma.Items.Components
CreateJoint(true);
#if SERVER
if (GameMain.Server != null)
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
item.CreateServerEvent(this);
}
@@ -272,10 +271,10 @@ namespace Barotrauma.Items.Components
CreateHulls();
}
if (door != null && DockingTarget.door != null)
if (Door != null && DockingTarget.Door != null)
{
WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => door.LinkedGap == wp.ConnectedGap);
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.door.LinkedGap == wp.ConnectedGap);
WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => Door.LinkedGap == wp.ConnectedGap);
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.Door.LinkedGap == wp.ConnectedGap);
if (myWayPoint != null && targetWayPoint != null)
{
@@ -333,19 +332,19 @@ namespace Barotrauma.Items.Components
{
if (DockingDir != 0) { return DockingDir; }
if (door != null)
if (Door != null)
{
if (door.LinkedGap.linkedTo.Count == 1)
if (Door.LinkedGap.linkedTo.Count == 1)
{
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 - Door.LinkedGap.linkedTo[0].WorldPosition.X) :
Math.Sign(Door.Item.WorldPosition.Y - Door.LinkedGap.linkedTo[0].WorldPosition.Y);
}
else if (dockingTarget?.door?.LinkedGap != null && dockingTarget.door.LinkedGap.linkedTo.Count == 1)
else if (dockingTarget?.Door?.LinkedGap != null && dockingTarget.Door.LinkedGap.linkedTo.Count == 1)
{
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(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);
}
}
if (dockingTarget != null)
@@ -367,24 +366,23 @@ namespace Barotrauma.Items.Components
private void ConnectWireBetweenPorts()
{
Wire wire = item.GetComponent<Wire>();
if (wire == null) return;
if (wire == null) { return; }
wire.Hidden = true;
wire.Locked = true;
wire.Hidden = true;
if (Item.Connections == null) return;
if (Item.Connections == null) { return; }
var powerConnection = Item.Connections.Find(c => c.IsPower);
if (powerConnection == null) return;
if (powerConnection == null) { return; }
if (DockingTarget == null || DockingTarget.item.Connections == null) return;
if (DockingTarget == null || DockingTarget.item.Connections == null) { return; }
var recipient = DockingTarget.item.Connections.Find(c => c.IsPower);
if (recipient == null) return;
if (recipient == null) { return; }
wire.RemoveConnection(item);
wire.RemoveConnection(DockingTarget.item);
powerConnection.TryAddLink(wire);
wire.Connect(powerConnection, false, false);
recipient.TryAddLink(wire);
@@ -399,13 +397,13 @@ namespace Barotrauma.Items.Components
doorBody = null;
}
Vector2 position = ConvertUnits.ToSimUnits(item.Position + (DockingTarget.door.Item.WorldPosition - item.WorldPosition));
Vector2 position = ConvertUnits.ToSimUnits(item.Position + (DockingTarget.Door.Item.WorldPosition - item.WorldPosition));
if (!MathUtils.IsValid(position))
{
string errorMsg =
"Attempted to create a door body at an invalid position (item pos: " + item.Position
+ ", item world pos: " + item.WorldPosition
+ ", docking target world pos: " + DockingTarget.door.Item.WorldPosition + ")\n" + Environment.StackTrace;
+ ", docking target world pos: " + DockingTarget.Door.Item.WorldPosition + ")\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
@@ -418,11 +416,11 @@ namespace Barotrauma.Items.Components
System.Diagnostics.Debug.Assert(doorBody == null);
doorBody = GameMain.World.CreateRectangle(
DockingTarget.door.Body.width,
DockingTarget.door.Body.height,
DockingTarget.Door.Body.width,
DockingTarget.Door.Body.height,
1.0f,
position);
doorBody.UserData = DockingTarget.door;
doorBody.UserData = DockingTarget.Door;
doorBody.CollisionCategories = Physics.CollisionWall;
doorBody.BodyType = BodyType.Static;
}
@@ -434,12 +432,12 @@ namespace Barotrauma.Items.Components
bodies = new Body[4];
if (DockingTarget.door != null)
if (DockingTarget.Door != null)
{
CreateDoorBody();
}
if (door != null)
if (Door != null)
{
DockingTarget.CreateDoorBody();
}
@@ -718,7 +716,7 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < 2; i++)
{
Gap doorGap = i == 0 ? door?.LinkedGap : DockingTarget?.door?.LinkedGap;
Gap doorGap = i == 0 ? Door?.LinkedGap : DockingTarget?.Door?.LinkedGap;
if (doorGap == null) continue;
doorGap.DisableHullRechecks = true;
if (doorGap.linkedTo.Count >= 2) continue;
@@ -773,10 +771,10 @@ namespace Barotrauma.Items.Components
DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine);
item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine);
if (door != null && DockingTarget.door != null)
if (Door != null && DockingTarget.Door != null)
{
WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => door.LinkedGap == wp.ConnectedGap);
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.door.LinkedGap == wp.ConnectedGap);
WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => Door.LinkedGap == wp.ConnectedGap);
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.Door.LinkedGap == wp.ConnectedGap);
if (myWayPoint != null && targetWayPoint != null)
{
@@ -838,7 +836,7 @@ namespace Barotrauma.Items.Components
obstructedWayPointsDisabled = false;
#if SERVER
if (GameMain.Server != null)
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
item.CreateServerEvent(this);
}
@@ -908,9 +906,9 @@ namespace Barotrauma.Items.Components
}
else
{
if (DockingTarget.door != null && doorBody != null)
if (DockingTarget.Door != null && doorBody != null)
{
doorBody.Enabled = DockingTarget.door.Body.Enabled;
doorBody.Enabled = DockingTarget.Door.Body.Enabled;
}
item.SendSignal(0, "1", "state_out", null);
@@ -927,6 +925,7 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
list.Remove(this);
hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null;
@@ -953,7 +952,7 @@ namespace Barotrauma.Items.Components
float distSqr = Vector2.DistanceSquared(item.Position, it.Position);
if (distSqr < closestDist)
{
door = doorComponent;
Door = doorComponent;
closestDist = distSqr;
}
}
@@ -982,7 +981,18 @@ namespace Barotrauma.Items.Components
{
InitializeLinks();
if (!item.linkedTo.Any()) return;
Wire wire = item.GetComponent<Wire>();
if (wire != null)
{
wire.Locked = true;
wire.Hidden = true;
if (wire.Connections.Contains(null))
{
wire.Drop(null);
}
}
if (!item.linkedTo.Any()) { return; }
List<MapEntity> linked = new List<MapEntity>(item.linkedTo);
foreach (MapEntity entity in linked)
@@ -995,6 +1005,7 @@ namespace Barotrauma.Items.Components
Dock(dockingPort);
}
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)

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