(965c31410a) Unstable v0.10.4.0
This commit is contained in:
@@ -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())
|
||||
{
|
||||
|
||||
+354
-117
@@ -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;
|
||||
|
||||
+8
-4
@@ -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,
|
||||
|
||||
+21
-2
@@ -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);
|
||||
|
||||
+1
-1
@@ -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)
|
||||
{
|
||||
|
||||
+27
-3
@@ -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)
|
||||
|
||||
+4
-3
@@ -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
|
||||
|
||||
+56
-34
@@ -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;
|
||||
}
|
||||
|
||||
+21
-4
@@ -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
|
||||
{
|
||||
|
||||
+246
-97
@@ -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)
|
||||
|
||||
+4
-2
@@ -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,
|
||||
|
||||
+6
-6
@@ -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)
|
||||
{
|
||||
|
||||
+3
-2
@@ -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;
|
||||
}
|
||||
|
||||
+8
-2
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+41
-36
@@ -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)
|
||||
|
||||
+7
-10
@@ -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))
|
||||
{
|
||||
|
||||
+59
-33
@@ -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();
|
||||
|
||||
+4
-97
@@ -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()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
+105
@@ -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)
|
||||
{
|
||||
|
||||
+8
-6
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+177
-29
@@ -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()
|
||||
|
||||
+19
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+84
-124
@@ -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
Reference in New Issue
Block a user