Unstable v0.1300.0.1

This commit is contained in:
Markus Isberg
2021-03-05 17:00:56 +02:00
parent 64cdb32078
commit cb969c959f
199 changed files with 6043 additions and 3911 deletions
@@ -232,8 +232,7 @@ namespace Barotrauma
public bool IsWithinSector(Vector2 worldPosition)
{
if (sectorRad >= MathHelper.TwoPi) return true;
if (sectorRad >= MathHelper.TwoPi) { return true; }
Vector2 diff = worldPosition - WorldPosition;
return MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir)) <= sectorRad * 0.5f;
}
@@ -298,7 +298,7 @@ namespace Barotrauma
private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget));
private string GetTargetingTag(AITarget aiTarget)
{
if (aiTarget.Entity == null) { return null; }
if (aiTarget?.Entity == null) { return null; }
string targetingTag = null;
if (aiTarget.Entity is Character targetCharacter)
{
@@ -377,6 +377,7 @@ namespace Barotrauma
{
if (DisableEnemyAI) { return; }
base.Update(deltaTime);
UpdateTriggers(deltaTime);
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager == insideSteering)
@@ -462,7 +463,7 @@ namespace Barotrauma
{
updateTargetsTimer -= deltaTime;
}
else if (avoidTimer <= 0)
else if (avoidTimer <= 0 || activeTriggers.Any() && returnTimer <= 0)
{
CharacterParams.TargetParams targetingParams = null;
UpdateTargets(Character, out targetingParams);
@@ -1448,8 +1449,12 @@ namespace Barotrauma
break;
case AttackPattern.Circle:
if (IsCoolDownRunning) { break; }
if (IsAttackRunning) { break; }
if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; }
if (selectedTargetingParams == null) { break; }
var targetSub = SelectedAiTarget.Entity?.Submarine;
if (targetSub == null) { break; }
float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2;
float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition);
switch (CirclePhase)
{
case CirclePhase.Start:
@@ -1471,22 +1476,31 @@ namespace Barotrauma
circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f)));
canAttack = false;
aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression);
CirclePhase = Vector2.DistanceSquared(WorldPosition, attackWorldPos) > MathUtils.Pow2(circleFallbackDistance) ? CirclePhase.CloseIn : CirclePhase.FallBack;
if (targetSub.Borders.Width < 1000)
{
breakCircling = true;
CirclePhase = CirclePhase.CloseIn;
}
else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance))
{
CirclePhase = CirclePhase.CloseIn;
}
else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.FallBack;
}
else
{
CirclePhase = CirclePhase.Advance;
}
break;
case CirclePhase.CloseIn:
var sub = SelectedAiTarget.Entity?.Submarine;
if (sub == null)
{
CirclePhase = CirclePhase.Start;
break;
}
if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(sub.Velocity))
if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity))
{
strikeTimer = AttackingLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
}
else if (!breakCircling && Vector2.DistanceSquared(WorldPosition, attackWorldPos) <= MathUtils.Pow2(circleFallbackDistance - 1000) &&
sub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
{
CirclePhase = CirclePhase.Advance;
}
@@ -1494,23 +1508,17 @@ namespace Barotrauma
break;
case CirclePhase.FallBack:
bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true);
if (isBlocked || Vector2.DistanceSquared(WorldPosition, attackWorldPos) > MathUtils.Pow2(circleFallbackDistance))
if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.Advance;
break;
}
return;
case CirclePhase.Advance:
var targetSub = SelectedAiTarget.Entity?.Submarine;
if (targetSub == null)
{
CirclePhase = CirclePhase.Start;
break;
}
Vector2 subSpeed = targetSub.Velocity;
float requiredDistMultiplier = 1;
// If the target sub is moving fast, just steer towards the target until close enough to strike
if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || distance > selectedTargetingParams.CircleStartDistance + 1000)
if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f))
{
CirclePhase = CirclePhase.CloseIn;
}
@@ -1532,7 +1540,7 @@ namespace Barotrauma
// When the offset position is outside of the sub it happens that the creature sometimes reaches the target point,
// which makes it continue circling around the point (as supposed)
// But when there is some offset and the offset is too near, this is not what we want.
if (targetSub.Borders.ContainsWorld(attackWorldPos + ConvertUnits.ToDisplayUnits(circleOffset)))
if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.Strike;
strikeTimer = AttackingLimb.attack.CoolDown;
@@ -1646,15 +1654,15 @@ namespace Barotrauma
return;
}
}
if (UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb))
{
CirclePhase = CirclePhase.Start;
}
else
if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb))
{
IgnoreTarget(SelectedAiTarget);
}
}
else if (IsAttackRunning)
{
AttackingLimb.attack.ResetAttackTimer();
}
}
private readonly List<Limb> attackLimbs = new List<Limb>();
@@ -2060,6 +2068,7 @@ namespace Barotrauma
targetValue = 0;
selectedTargetMemory = null;
targetingParams = null;
bool isAnyTargetClose = false;
foreach (AITarget aiTarget in AITarget.List)
{
@@ -2163,6 +2172,14 @@ namespace Barotrauma
continue;
}
}
if (door == null)
{
// Ignore items inside ruins, unless we are in the same hull. We can't target the ruin walls.
if (item.Submarine == null && item.CurrentHull != Character.CurrentHull)
{
continue;
}
}
foreach (var prio in AIParams.Targets)
{
if (item.HasTag(prio.Tag))
@@ -2379,18 +2396,16 @@ namespace Barotrauma
}
}
}
if (!aiTarget.IsWithinSector(WorldPosition)) { continue; }
Vector2 toTarget = aiTarget.WorldPosition - character.WorldPosition;
float dist = toTarget.Length();
float nonModifiedDist = dist;
//if the target has been within range earlier, the character will notice it more easily
if (targetMemories.ContainsKey(aiTarget))
{
dist *= 0.9f;
}
if (!CanPerceive(aiTarget, dist)) { continue; }
if (!aiTarget.IsWithinSector(WorldPosition)) { continue; }
//if the target is very close, the distance doesn't make much difference
// -> just ignore the distance and attack whatever has the highest priority
@@ -2405,14 +2420,26 @@ namespace Barotrauma
if (targetParams.AttackPattern == AttackPattern.Circle)
{
if (Character.Submarine == null && aiTarget.Entity?.Submarine != null)
if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && !isAnyTargetClose)
{
if (Submarine.MainSubs.Contains(aiTarget.Entity.Submarine))
{
// Prioritize targets that are near the horizontal center of the sub
// Prioritize targets that are near the horizontal center of the sub, but only when none of the targets is reachable.
float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X);
dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter));
}
else
{
dist *= 5;
}
}
}
// Don't target characters that are outside of the allowed zone, unless attacking or escaping
if (targetParams.State != AIState.Attack && targetParams.State != AIState.Escape && targetParams.State != AIState.Avoid)
{
if (!IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
{
continue;
}
}
@@ -2486,11 +2513,26 @@ namespace Barotrauma
}
}
}
else if (targetCharacter.Submarine == null && Character.Submarine == null)
{
// Ignore the target when it's far enough and blocked by the level geometry, because the steering avoidance probably can't get us to the target.
if (dist > Math.Clamp(ConvertUnits.ToDisplayUnits(colliderLength) * 10, 1000, 5000))
{
if (Submarine.PickBodies(SimPosition, targetCharacter.SimPosition, collisionCategory: Physics.CollisionLevel).Any())
{
continue;
}
}
}
}
newTarget = aiTarget;
selectedTargetMemory = targetMemory;
targetValue = valueModifier;
targetingParams = targetParams;
if (!isAnyTargetClose)
{
isAnyTargetClose = ConvertUnits.ToDisplayUnits(colliderLength) > nonModifiedDist;
}
}
}
@@ -2619,7 +2661,7 @@ namespace Barotrauma
}
}
}
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null)
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null && selectedTargetingParams?.AttackPattern == AttackPattern.Straight)
{
if (closestBody.UserData is Structure w && w.Submarine != null && w.Submarine == SelectedAiTarget.Entity?.Submarine ||
closestBody.UserData is Item i && i.Submarine != null && i.Submarine == SelectedAiTarget.Entity?.Submarine)
@@ -2753,6 +2795,44 @@ namespace Barotrauma
private readonly float stateResetCooldown = 10;
private float stateResetTimer;
private bool isStateChanged;
private readonly Dictionary<AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<AITrigger, CharacterParams.TargetParams>();
private readonly HashSet<AITrigger> inactiveTriggers = new HashSet<AITrigger>();
public void LaunchTrigger(AITrigger trigger)
{
if (trigger.IsTriggered) { return; }
if (activeTriggers.ContainsKey(trigger)) { return; }
if (activeTriggers.ContainsValue(selectedTargetingParams))
{
if (!trigger.AllowToOverride) { return; }
var existingTrigger = activeTriggers.FirstOrDefault(kvp => kvp.Value == selectedTargetingParams && kvp.Key.AllowToBeOverridden);
if (existingTrigger.Key == null) { return; }
activeTriggers.Remove(existingTrigger.Key);
}
trigger.Launch();
activeTriggers.Add(trigger, selectedTargetingParams);
ChangeParams(selectedTargetingParams, trigger.State);
}
private void UpdateTriggers(float deltaTime)
{
foreach (var triggerObject in activeTriggers)
{
AITrigger trigger = triggerObject.Key;
trigger.UpdateTimer(deltaTime);
if (!trigger.IsActive)
{
trigger.Reset();
ResetParams(triggerObject.Value);
inactiveTriggers.Add(trigger);
}
}
foreach (AITrigger trigger in inactiveTriggers)
{
activeTriggers.Remove(trigger);
}
inactiveTriggers.Clear();
}
/// <summary>
/// Resets the target's state to the original value defined in the xml.
@@ -2768,11 +2848,7 @@ namespace Barotrauma
tempParams.Values.ForEach(t => AIParams.RemoveTarget(t));
tempParams.Remove(tag);
}
targetParams.Reset();
ResetAITarget();
// Enforce the idle state so that we don't keep following the target if there's one
State = AIState.Idle;
PreviousState = AIState.Idle;
ResetParams(targetParams);
return true;
}
else
@@ -2784,6 +2860,27 @@ namespace Barotrauma
private readonly Dictionary<string, CharacterParams.TargetParams> modifiedParams = new Dictionary<string, CharacterParams.TargetParams>();
private readonly Dictionary<string, CharacterParams.TargetParams> tempParams = new Dictionary<string, CharacterParams.TargetParams>();
private void ChangeParams(CharacterParams.TargetParams targetParams, AIState state, float? priority = null)
{
if (targetParams == null) { return; }
if (priority.HasValue)
{
targetParams.Priority = priority.Value;
}
targetParams.State = state;
}
private void ResetParams(CharacterParams.TargetParams targetParams)
{
targetParams?.Reset();
if (selectedTargetingParams == targetParams || State == AIState.Idle)
{
ResetAITarget();
State = AIState.Idle;
PreviousState = AIState.Idle;
}
}
private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false)
{
if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
@@ -2938,45 +3035,56 @@ namespace Barotrauma
}
}
private bool IsPositionInsideAllowedZone(Vector2 pos, out Vector2 targetDir)
{
targetDir = Vector2.Zero;
if (AIParams.AvoidAbyss)
{
if (pos.Y < Level.Loaded.AbyssStart)
{
// Too far down
targetDir = Vector2.UnitY;
}
}
if (AIParams.StayInAbyss)
{
if (pos.Y > Level.Loaded.AbyssStart)
{
// Too far up
targetDir = -Vector2.UnitY;
}
else if (pos.Y < Level.Loaded.AbyssEnd)
{
// Too far down
targetDir = Vector2.UnitY;
}
}
float margin = 30000;
if (pos.X < -margin)
{
// Too far left
targetDir = Vector2.UnitX;
}
else if (pos.X > Level.Loaded.Size.X + margin)
{
// Too far right
targetDir = -Vector2.UnitX;
}
return targetDir == Vector2.Zero;
}
private Vector2 returnDir;
private float returnTimer;
private void SteerInsideLevel(float deltaTime)
{
if (State == AIState.Attack) { return; }
if (SteeringManager is IndoorsSteeringManager) { return; }
if (Level.Loaded == null) { return; }
Point levelSize = Level.Loaded.Size;
float returnTime = 10;
if (AIParams.AvoidAbyss)
if (State == AIState.Attack && returnTimer <= 0) { return; }
float returnTime = 5;
if (!IsPositionInsideAllowedZone(WorldPosition, out Vector2 targetDir))
{
if (WorldPosition.Y < Level.Loaded.AbyssStart)
{
// Too far down
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitY;
}
}
else if (AIParams.StayInAbyss)
{
if (WorldPosition.Y > Level.Loaded.AbyssStart)
{
// Too far up
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = -Vector2.UnitY;
}
}
float margin = AIParams.AvoidAbyss ? 0 : 30000;
if (WorldPosition.X < margin)
{
// Too far left
returnDir = targetDir;
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitX;
}
if (WorldPosition.X > levelSize.X + margin)
{
// Too far right
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = -Vector2.UnitX;
}
if (returnTimer > 0)
{
@@ -15,7 +15,7 @@ namespace Barotrauma
private readonly AIObjectiveManager objectiveManager;
private float sortTimer;
public float SortTimer { get; set; }
private float crouchRaycastTimer;
private float reactTimer;
private float unreachableClearTimer;
@@ -131,7 +131,7 @@ namespace Barotrauma
outsideSteering = new SteeringManager(this);
objectiveManager = new AIObjectiveManager(c);
reactTimer = GetReactionTime();
sortTimer = Rand.Range(0f, sortObjectiveInterval);
SortTimer = Rand.Range(0f, sortObjectiveInterval);
}
public override void Update(float deltaTime)
@@ -218,6 +218,7 @@ namespace Barotrauma
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != Character.Submarine) { continue; }
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
if (IsFriendly(c)) { continue; }
Vector2 toTarget = c.WorldPosition - WorldPosition;
float dist = toTarget.LengthSquared();
@@ -264,14 +265,14 @@ namespace Barotrauma
CheckCrouching(deltaTime);
Character.ClearInputs();
if (sortTimer > 0.0f)
if (SortTimer > 0.0f)
{
sortTimer -= deltaTime;
SortTimer -= deltaTime;
}
else
{
objectiveManager.SortObjectives();
sortTimer = sortObjectiveInterval;
SortTimer = sortObjectiveInterval;
}
objectiveManager.UpdateObjectives(deltaTime);
@@ -288,14 +289,14 @@ namespace Barotrauma
{
if (Character.CurrentHull != null)
{
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
if (Character.IsOnPlayerTeam)
{
// Outpost npcs don't inform each other about threats, like crew members do.
VisibleHulls.ForEach(h => RefreshHullSafety(h));
VisibleHulls.ForEach(h => PropagateHullSafety(Character, h));
}
else
{
VisibleHulls.ForEach(h => PropagateHullSafety(Character, h));
// Outpost npcs don't inform each other about threats, like crew members do.
VisibleHulls.ForEach(h => RefreshHullSafety(h));
}
}
if (Character.SpeechImpediment < 100.0f)
@@ -1065,7 +1066,9 @@ namespace Barotrauma
{
if (!IsFriendly(attacker))
{
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
return c.AIController is HumanAIController humanAI &&
(humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders))
? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
}
else
{
@@ -1192,7 +1195,7 @@ namespace Barotrauma
{
base.Reset();
objectiveManager.SortObjectives();
sortTimer = sortObjectiveInterval;
SortTimer = sortObjectiveInterval;
float waitDuration = characterWaitOnSwitch;
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
{
@@ -1418,6 +1421,9 @@ namespace Barotrauma
item.StolenDuringRound = true;
otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f);
someoneSpoke = true;
#if CLIENT
HintManager.OnStoleItem(thief, item);
#endif
}
// React if we are security
if (!TriggerSecurity(otherHumanAI))
@@ -1554,7 +1560,7 @@ namespace Barotrauma
targetAdded = true;
}
}
}, (caller.AIController as HumanAIController)?.ReportRange ?? float.PositiveInfinity);
}, range: (caller.AIController as HumanAIController)?.ReportRange ?? float.PositiveInfinity);
return targetAdded;
}
@@ -1726,11 +1732,9 @@ namespace Barotrauma
switch (myTeam)
{
case CharacterTeamType.None:
// Only enemies are in the Team "None"
return false;
case CharacterTeamType.Team1:
case CharacterTeamType.Team2:
// Team1 is only friendly to Team1 and friendly NPCs
// Only friendly to the same team and friendly NPCs
return otherTeam == CharacterTeamType.FriendlyNPC;
case CharacterTeamType.FriendlyNPC:
// Friendly NPCs are friendly to both teams
@@ -221,6 +221,19 @@ namespace Barotrauma
{
currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType);
}
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode &&
(campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false))
{
if (speaker.TeamID == CharacterTeamType.None)
{
currentFlags.Add("Bandit");
}
else if (speaker.TeamID == CharacterTeamType.FriendlyNPC)
{
currentFlags.Add("Hostage");
}
}
}
return currentFlags;
@@ -79,6 +79,7 @@ namespace Barotrauma
private float coolDownTimer;
private IEnumerable<Body> myBodies;
private float aimTimer;
private float reloadTimer;
private float spreadTimer;
private bool canSeeTarget;
@@ -147,6 +148,7 @@ namespace Barotrauma
Mode = CombatMode.Retreat;
}
spreadTimer = Rand.Range(-10, 10);
HumanAIController.SortTimer = 0;
}
public override float GetPriority()
@@ -170,6 +172,10 @@ namespace Barotrauma
base.Update(deltaTime);
ignoreWeaponTimer -= deltaTime;
checkWeaponsTimer -= deltaTime;
if (reloadTimer > 0)
{
reloadTimer -= deltaTime;
}
if (ignoreWeaponTimer < 0)
{
ignoredWeapons.Clear();
@@ -219,7 +225,11 @@ namespace Barotrauma
{
OperateWeapon(deltaTime);
}
if (!HoldPosition && seekAmmunitionObjective == null && seekWeaponObjective == null)
if (HoldPosition)
{
SteeringManager.Reset();
}
else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
Move(deltaTime);
}
@@ -641,7 +651,7 @@ namespace Barotrauma
var slots = Weapon.AllowedSlots.Where(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
aimTimer = Rand.Range(1f, 1.5f) / AimSpeed;
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
}
else
{
@@ -912,7 +922,7 @@ namespace Barotrauma
}
if (!canSeeTarget)
{
aimTimer = Rand.Range(0.2f, 1f) / AimSpeed;
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
return;
}
if (Weapon.RequireAimToUse)
@@ -930,6 +940,7 @@ namespace Barotrauma
aimTimer -= deltaTime;
return;
}
if (reloadTimer > 0) { return; }
if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 1) { return; }
if (holdFireCondition != null && holdFireCondition()) { return; }
float sqrDist = Vector2.DistanceSquared(character.Position, Enemy.Position);
@@ -1010,18 +1021,25 @@ namespace Barotrauma
private void UseWeapon(float deltaTime)
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
reloadTime = rangedWeapon.Reload;
// If the weapon is just equipped, we can't shoot just yet.
if (rangedWeapon.ReloadTimer <= 0)
{
reloadTime = rangedWeapon.Reload;
}
}
if (WeaponComponent is MeleeWeapon mw)
{
reloadTime = mw.Reload;
if (!((HumanoidAnimController)character.AnimController).Crouching)
{
reloadTime = mw.Reload;
}
}
aimTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.5f) / AimSpeed);
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
}
protected override void OnCompleted()
@@ -1031,10 +1049,7 @@ namespace Barotrauma
{
Unequip();
}
if (!HoldPosition)
{
SteeringManager.Reset();
}
SteeringManager.Reset();
}
protected override void OnAbandon()
@@ -1044,10 +1059,7 @@ namespace Barotrauma
{
Unequip();
}
if (!HoldPosition)
{
SteeringManager.Reset();
}
SteeringManager.Reset();
}
public override void Reset()
@@ -10,6 +10,8 @@ namespace Barotrauma
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
protected override float TargetUpdateTimeMultiplier => 0.2f;
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
@@ -48,7 +50,8 @@ namespace Barotrauma
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target == null || target.Removed) { return false; }
if (target.IsDead || target.IsUnconscious) { return false; }
if (target == character) { return false; }
if (target.Submarine == null) { return false; }
if (character.Submarine == null) { return false; }
@@ -555,6 +555,13 @@ namespace Barotrauma
//otherwise characters can let go of the ladders too soon once they're close enough to the target
if (PathSteering.CurrentPath.NextNode != null) { return false; }
}
if (!character.AnimController.InWater)
{
float yDiff = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y);
if (yDiff > CloseEnough) { return false; }
float xDiff = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X);
return xDiff <= CloseEnough;
}
return Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
}
}
@@ -11,6 +11,7 @@ namespace Barotrauma
protected HashSet<T> ignoreList = new HashSet<T>();
private float ignoreListTimer;
protected float targetUpdateTimer;
protected virtual float TargetUpdateTimeMultiplier { get; } = 1;
private float syncTimer;
private readonly float syncTime = 1;
@@ -61,7 +62,7 @@ namespace Barotrauma
ignoreListTimer += deltaTime;
}
}
if (targetUpdateTimer < 0)
if (targetUpdateTimer <= 0)
{
UpdateTargets();
}
@@ -69,9 +70,9 @@ namespace Barotrauma
{
targetUpdateTimer -= deltaTime;
}
if (syncTimer < 0)
if (syncTimer <= 0)
{
syncTimer = syncTime * Rand.Range(0.9f, 1.1f);
syncTimer = Math.Min(syncTime * Rand.Range(0.9f, 1.1f), targetUpdateTimer);
// Sync objectives, subobjectives and targets
foreach (var objective in Objectives)
{
@@ -95,7 +96,7 @@ namespace Barotrauma
}
// the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
private float SetTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1);
private float CalculateTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1) * TargetUpdateTimeMultiplier;
public override void Reset()
{
@@ -156,7 +157,7 @@ namespace Barotrauma
protected void UpdateTargets()
{
SetTargetUpdateTimer();
CalculateTargetUpdateTimer();
Targets.Clear();
FindTargets();
CreateObjectives();
@@ -386,7 +386,9 @@ namespace Barotrauma
Abandon = true;
return false;
}
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
bool isCompleted =
AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) ||
targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold);
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
@@ -25,8 +25,8 @@ namespace Barotrauma
{
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
// On the other hand, if the bots too eagerly heal characters when it's not nevessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
// On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
}
}
@@ -83,7 +83,7 @@ namespace Barotrauma
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
if (!humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveRescueAll>())
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
{
if (!character.IsMedic && target != character)
{
@@ -281,7 +281,7 @@ namespace Barotrauma
}
}
public const float MAX_SPEED = 30;
public const float MAX_SPEED = 20;
public Vector2 TargetMovement
{
@@ -636,9 +636,12 @@ namespace Barotrauma
//always collides with bodies other than structures
if (!(f2.Body.UserData is Structure structure))
{
lock (impactQueue)
if (!f2.IsSensor)
{
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
lock (impactQueue)
{
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
}
}
return true;
}
@@ -156,6 +156,8 @@ namespace Barotrauma
public Entity LastDamageSource;
public AttackResult LastDamage;
public float InvisibleTimer;
private CharacterPrefab prefab;
@@ -199,7 +201,12 @@ namespace Barotrauma
set => Params.Visibility = value;
}
public bool IsTraitor;
public bool IsTraitor
{
get;
set;
}
public string TraitorCurrentObjective = "";
public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase);
public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male;
@@ -333,6 +340,7 @@ namespace Barotrauma
//text displayed when the character is highlighted if custom interact is set
public string customInteractHUDText;
private Action<Character, Character> onCustomInteract;
public ConversationAction ActiveConversation;
public bool AllowCustomInteract
{
@@ -349,6 +357,9 @@ namespace Barotrauma
set
{
lockHandsTimer = MathHelper.Clamp(lockHandsTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f);
#if CLIENT
HintManager.OnHandcuffed(this);
#endif
}
}
@@ -605,6 +616,9 @@ namespace Barotrauma
get => _selectedConstruction;
set
{
#if CLIENT
HintManager.OnSetSelectedConstruction(this, _selectedConstruction, value);
#endif
_selectedConstruction = value;
#if CLIENT
if (Controlled == this)
@@ -1661,6 +1675,12 @@ namespace Barotrauma
{
item.Use(deltaTime, this);
}
#if CLIENT
else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
{
HintManager.OnShootWithoutAiming(this, item);
}
#endif
}
}
}
@@ -1853,6 +1873,19 @@ namespace Barotrauma
return false;
}
public Item GetEquippedItem(string tagOrIdentifier)
{
if (Inventory == null) { return null; }
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; }
var item = Inventory.GetItemAt(i);
if (item == null) { continue; }
if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; }
}
return null;
}
public bool CanAccessInventory(Inventory inventory)
{
if (!CanInteract || inventory.Locked) { return false; }
@@ -2857,6 +2890,7 @@ namespace Barotrauma
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
@@ -3268,12 +3302,13 @@ namespace Barotrauma
//#endif
// }
SetStun(stun);
if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
{
if (attacker.TeamID == TeamID) { return new AttackResult(); }
}
SetStun(stun);
Vector2 dir = hitLimb.WorldPosition - worldPosition;
if (Math.Abs(attackImpulse) > 0.0f)
{
@@ -3308,6 +3343,7 @@ namespace Barotrauma
};
if (attackResult.Damage > 0)
{
LastDamage = attackResult;
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
if (attacker != null)
@@ -153,6 +153,8 @@ namespace Barotrauma
private static ushort idCounter;
private const string disguiseName = "???";
public bool HasNickname => Name != OriginalName;
public string OriginalName { get; private set; }
public string Name;
public string DisplayName
{
@@ -453,7 +455,7 @@ namespace Barotrauma
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
// Used for creating the data
public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced)
public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
@@ -503,6 +505,7 @@ namespace Barotrauma
}
}
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId);
Salary = CalculateSalary();
if (ragdollFileName != null)
@@ -518,6 +521,7 @@ namespace Barotrauma
ID = idCounter;
idCounter++;
Name = infoElement.GetAttributeString("name", "");
OriginalName = infoElement.GetAttributeString("originalname", null);
string genderStr = infoElement.GetAttributeString("gender", "male").ToLowerInvariant();
Salary = infoElement.GetAttributeInt("salary", 1000);
Enum.TryParse(infoElement.GetAttributeString("race", "White"), true, out Race race);
@@ -576,6 +580,11 @@ namespace Barotrauma
}
}
if (string.IsNullOrEmpty(OriginalName))
{
OriginalName = Name;
}
StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false);
string personalityName = infoElement.GetAttributeString("personality", "");
ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty);
@@ -622,7 +631,17 @@ namespace Barotrauma
public int GetIdentifier()
{
int id = ToolBox.StringToInt(Name);
return GetIdentifier(Name);
}
public int GetIdentifierUsingOriginalName()
{
return GetIdentifier(OriginalName);
}
private int GetIdentifier(string name)
{
int id = ToolBox.StringToInt(name);
id ^= HeadSpriteId;
id ^= (int)Race << 6;
id ^= HairIndex << 12;
@@ -939,12 +958,24 @@ namespace Barotrauma
partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos);
public void Rename(string newName)
{
if (string.IsNullOrEmpty(newName)) { return; }
Name = newName;
}
public void ResetName()
{
Name = OriginalName;
}
public XElement Save(XElement parentElement)
{
XElement charElement = new XElement("Character");
charElement.Add(
new XAttribute("name", Name),
new XAttribute("originalname", OriginalName),
new XAttribute("speciesname", SpeciesName),
new XAttribute("gender", Head.gender == Gender.Male ? "male" : "female"),
new XAttribute("race", Head.race.ToString()),
@@ -957,7 +988,7 @@ namespace Barotrauma
new XAttribute("startitemsgiven", StartItemsGiven),
new XAttribute("ragdoll", ragdollFileName),
new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name));
// TODO: animations?
if (Character != null)
@@ -260,10 +260,13 @@ namespace Barotrauma
/// <summary>
/// Use this method to skip clamping and additional logic of the setters.
/// Intended only to be used when the value is already clamped! (networking code)
/// Ideally we would keep this private, but doing so would require too much refactoring.
/// </summary>
public void SetStrength(float strength) => _strength = strength;
public void SetStrength(float strength)
{
_nonClampedStrength = strength;
_strength = _nonClampedStrength;
}
public bool ShouldShowIcon(Character afflictedCharacter)
{
@@ -290,6 +290,9 @@ namespace Barotrauma
//how high the strength has to be for the affliction icon to be shown with a health scanner
public readonly float ShowInHealthScannerThreshold = 0.05f;
//how strong the affliction needs to be before bots attempt to treat it
public readonly float TreatmentThreshold = 5.0f;
//how much karma changes when a player applies this affliction to someone (per strength of the affliction)
public float KarmaChangeOnApplied;
@@ -376,6 +379,9 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Cannot override all afflictions, because many of them are required by the main game! Please try overriding them one by one.");
}
List<(AfflictionPrefab prefab, XElement element)> loadedAfflictions = new List<(AfflictionPrefab prefab, XElement element)>();
foreach (XElement element in mainElement.Elements())
{
bool isOverride = element.IsOverride();
@@ -510,10 +516,18 @@ namespace Barotrauma
if (prefab != null)
{
loadedAfflictions.Add((prefab, element));
Prefabs.Add(prefab, isOverride);
prefab.CalculatePrefabUIntIdentifier(Prefabs);
}
}
//load the effects after all the afflictions in the file have been instantiated
//otherwise afflictions can't inflict other afflictions that are defined at a later point in the file
foreach ((AfflictionPrefab prefab, XElement element) in loadedAfflictions)
{
prefab.LoadEffects(element);
}
}
public static void RemoveByFile(string filePath)
@@ -565,6 +579,7 @@ namespace Barotrauma
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f));
TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f));
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f);
@@ -584,9 +599,6 @@ namespace Barotrauma
case "icon":
Icon = new Sprite(subElement);
break;
case "effect":
effects.Add(new Effect(subElement, Name));
break;
case "periodiceffect":
periodicEffects.Add(new PeriodicEffect(subElement, Name));
break;
@@ -614,6 +626,19 @@ namespace Barotrauma
constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) });
}
private void LoadEffects(XElement element)
{
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "effect":
effects.Add(new Effect(subElement, Name));
break;
}
}
}
public override string ToString()
{
return "AfflictionPrefab (" + Name + ")";
@@ -36,7 +36,11 @@ namespace Barotrauma
public LimbHealth(XElement element, CharacterHealth characterHealth)
{
Name = TextManager.Get("HealthLimbName." + element.GetAttributeString("name", ""));
string limbName = element.GetAttributeString("name", null) ?? "generic";
if (limbName != "generic")
{
Name = TextManager.Get("HealthLimbName." + limbName);
}
this.characterHealth = characterHealth;
foreach (XElement subElement in element.Elements())
{
@@ -664,7 +668,6 @@ namespace Barotrauma
}
}
partial void UpdateProjSpecific(float deltaTime);
partial void UpdateLimbAfflictionOverlays();
@@ -687,6 +690,10 @@ namespace Barotrauma
{
var affliction = limbHealths[i].Afflictions[j];
Limb targetLimb = Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == i);
if (targetLimb == null)
{
targetLimb = Character.AnimController.MainLimb;
}
affliction.Update(this, targetLimb, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
if (affliction is AfflictionBleeding bleeding)
@@ -877,6 +884,7 @@ namespace Barotrauma
float minSuitability = -10, maxSuitability = 10;
foreach (Affliction affliction in GetAllAfflictions())
{
if (affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
foreach (KeyValuePair<string, float> treatment in affliction.Prefab.TreatmentSuitability)
{
if (!treatmentSuitability.ContainsKey(treatment.Key))
@@ -20,6 +20,9 @@ namespace Barotrauma
[Serialize(1f, false)]
public float HealthMultiplier { get; protected set; }
[Serialize(1f, false)]
public float HealthMultiplierInMultiplayer { get; protected set; }
[Serialize(1f, false)]
public float AimSpeed { get; protected set; }
@@ -117,6 +120,10 @@ namespace Barotrauma
public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null)
{
npc.CharacterHealth.MaxVitality *= HealthMultiplier;
if (GameMain.NetworkMember != null)
{
npc.CharacterHealth.MaxVitality *= HealthMultiplierInMultiplayer;
}
var humanAI = npc.AIController as HumanAIController;
if (humanAI != null)
{
@@ -732,6 +732,10 @@ namespace Barotrauma
{
newAffliction = affliction.CreateMultiplied(finalDamageModifier);
}
else
{
newAffliction.SetStrength(affliction.NonClampedStrength);
}
if (applyAffliction)
{
@@ -430,10 +430,10 @@ namespace Barotrauma
[Serialize(false, true)]
public bool UseHealthWindow { get; set; }
[Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
public float BleedingReduction { get; private set; }
[Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
public float BurnReduction { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
@@ -707,9 +707,10 @@ namespace Barotrauma
commands.Add(new Command("freecamera|freecam", "freecam: Detach the camera from the controlled character.", (string[] args) =>
{
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { return; }
Character.Controlled = null;
GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
#if CLIENT
GameMain.Client?.SendConsoleCommand("freecam");
#endif
}, isCheat: true));
@@ -1695,6 +1696,8 @@ namespace Barotrauma
return null;
}
// Use same sorting as DebugConsole.ListCharacterNames() above
matchingCharacters = matchingCharacters.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ToList();
if (characterIndex == -1)
{
if (matchingCharacters.Count > 1)
@@ -179,6 +179,7 @@ namespace Barotrauma
{
if (speaker == null) { return; }
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
speaker.ActiveConversation = this;
speaker.SetCustomInteract(null, null);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
@@ -213,24 +214,24 @@ namespace Barotrauma
#if CLIENT
Character.DisableControls = true;
#endif
if (ShouldInterrupt())
if (ShouldInterrupt())
{
ResetSpeaker();
interrupt = true;
interrupt = true;
}
return;
return;
}
if (!string.IsNullOrEmpty(SpeakerTag))
{
if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk) { return; }
if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; }
speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character;
if (speaker == null || speaker.Removed)
{
return;
{
return;
}
//some conversation already assigned to the speaker, wait for it to be removed
if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk)
if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent)
{
return;
}
@@ -241,6 +242,7 @@ namespace Barotrauma
else
{
speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk;
speaker.ActiveConversation = this;
#if CLIENT
speaker.SetCustomInteract(
TryStartConversation,
@@ -18,14 +18,13 @@ namespace Barotrauma
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.");
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": 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.");
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored.");
}
}
@@ -240,7 +240,7 @@ namespace Barotrauma
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable());
}
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<string> moduleFlags = null, IEnumerable<string> spawnpointTags = null)
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<string> moduleFlags = null, IEnumerable<string> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false)
{
List<WayPoint> potentialSpawnPoints = spawnLocation switch
{
@@ -253,6 +253,7 @@ namespace Barotrauma
};
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed);
var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Contains("airlock") ?? false).ToList();
if (moduleFlags != null && moduleFlags.Any())
{
@@ -282,7 +283,7 @@ namespace Barotrauma
IEnumerable<WayPoint> validSpawnPoints;
if (spawnPointType.HasValue)
{
validSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.SpawnType == spawnPointType.Value);
validSpawnPoints = potentialSpawnPoints.FindAll(wp => spawnPointType.Value.HasFlag(wp.SpawnType));
}
else
{
@@ -291,7 +292,6 @@ namespace Barotrauma
}
//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);
@@ -313,7 +313,25 @@ namespace Barotrauma
}
}
return validSpawnPoints.GetRandom();
if (asFarAsPossibleFromAirlock && airlockSpawnPoints.Any())
{
WayPoint furthestPoint = validSpawnPoints.First();
float furthestDist = 0.0f;
foreach (WayPoint waypoint in validSpawnPoints)
{
float dist = Vector2.DistanceSquared(waypoint.WorldPosition, airlockSpawnPoints.First().WorldPosition);
if (dist > furthestDist)
{
furthestDist = dist;
furthestPoint = waypoint;
}
}
return furthestPoint;
}
else
{
return validSpawnPoints.GetRandom();
}
}
public override string ToDebugString()
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using NLog;
namespace Barotrauma
{
@@ -13,7 +14,8 @@ namespace Barotrauma
{
CONVERSATION,
STATUSEFFECT,
MISSION
MISSION,
UNLOCKPATH
}
const float IntensityUpdateInterval = 5.0f;
@@ -111,21 +113,50 @@ namespace Barotrauma
SelectSettings();
var initialEventSet = SelectRandomEvents(EventSet.List);
if (initialEventSet != null)
int seed = 0;
if (level != null)
{
pendingEventSets.Add(initialEventSet);
int seed = ToolBox.StringToInt(level.Seed);
seed = ToolBox.StringToInt(level.Seed);
foreach (var previousEvent in level.LevelData.EventHistory)
{
seed ^= ToolBox.StringToInt(previousEvent.Identifier);
}
MTRandom rand = new MTRandom(seed);
}
MTRandom rand = new MTRandom(seed);
var initialEventSet = SelectRandomEvents(EventSet.List);
if (initialEventSet != null)
{
pendingEventSets.Add(initialEventSet);
CreateEvents(initialEventSet, rand);
}
if (level?.LevelData?.Type == LevelData.LevelType.Outpost)
{
//if the outpost is connected to a locked connection, create an event to unlock it
if (level.StartLocation?.Connections.Any(c => c.Locked) ?? false)
{
var unlockPathPrefabs = EventSet.PrefabList.FindAll(e => e.UnlockPathEvent);
var unlockPathPrefabsForBiome = unlockPathPrefabs.FindAll(e =>
string.IsNullOrEmpty(e.BiomeIdentifier) ||
e.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase));
var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ?
ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, unlockPathPrefabsForBiome.Select(b => b.Commonness).ToList(), rand) :
ToolBox.SelectWeightedRandom(unlockPathPrefabs, unlockPathPrefabs.Select(b => b.Commonness).ToList(), rand);
if (unlockPathEventPrefab != null)
{
var newEvent = unlockPathEventPrefab.CreateInstance();
newEvent.Init(true);
ActiveEvents.Add(newEvent);
}
else
{
//if no event that unlocks the path can be found, unlock it automatically
level.StartLocation.Connections.ForEach(c => c.Locked = false);
}
}
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
{
@@ -362,20 +393,24 @@ namespace Barotrauma
}
else if (eventSet.PerWreck)
{
var wrecks = Submarine.Loaded.Where(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive));
var wrecks = Submarine.Loaded.Where(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive));
applyCount = wrecks.Count();
foreach (var wreck in wrecks)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Submarine == wreck; });
}
}
var suitablePrefabs = eventSet.EventPrefabs.FindAll(e =>
string.IsNullOrEmpty(e.First.BiomeIdentifier) ||
e.First.BiomeIdentifier.Equals(Level.Loaded.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase));
for (int i = 0; i < applyCount; i++)
{
if (eventSet.ChooseRandom)
{
if (eventSet.EventPrefabs.Count > 0)
if (suitablePrefabs.Count > 0)
{
List<Pair<EventPrefab, float>> unusedEvents = new List<Pair<EventPrefab, float>>(eventSet.EventPrefabs);
List<Pair<EventPrefab, float>> unusedEvents = new List<Pair<EventPrefab, float>>(suitablePrefabs);
for (int j = 0; j < eventSet.EventCount; j++)
{
var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => CalculateCommonness(e)).ToList(), rand);
@@ -403,7 +438,7 @@ namespace Barotrauma
}
else
{
foreach (Pair<EventPrefab, float> eventPrefab in eventSet.EventPrefabs)
foreach (Pair<EventPrefab, float> eventPrefab in suitablePrefabs)
{
var newEvent = eventPrefab.First.CreateInstance();
if (newEvent == null) { continue; }
@@ -430,12 +465,19 @@ namespace Barotrauma
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var allowedEventSets =
eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && level.LevelData.Type == es.LevelType);
eventSets.Where(es =>
level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty &&
level.LevelData.Type == es.LevelType &&
(string.IsNullOrEmpty(es.BiomeIdentifier) || es.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase)));
LocationType locationType = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation?.Type ?? level?.StartLocation?.Type;
if (locationType != null)
Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation;
LocationType locationType = location?.GetLocationType();
if (location != null)
{
allowedEventSets = allowedEventSets.Where(set => set.LocationTypeIdentifiers == null || set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, locationType.Identifier, StringComparison.OrdinalIgnoreCase)));
allowedEventSets = allowedEventSets.Where(set =>
set.LocationTypeIdentifiers == null ||
set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, locationType.Identifier, StringComparison.OrdinalIgnoreCase)));
}
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
@@ -12,6 +12,8 @@ namespace Barotrauma
public readonly bool TriggerEventCooldown;
public float Commonness;
public string Identifier;
public bool UnlockPathEvent;
public string BiomeIdentifier;
public EventPrefab(XElement element)
{
@@ -34,6 +36,8 @@ namespace Barotrauma
Commonness = element.GetAttributeFloat("commonness", 1.0f);
SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false);
BiomeIdentifier = ConfigElement.GetAttributeString("biome", string.Empty);
}
public Event CreateInstance()
@@ -65,6 +65,8 @@ namespace Barotrauma
//0-100
public readonly float MinLevelDifficulty, MaxLevelDifficulty;
public readonly string BiomeIdentifier;
public readonly LevelData.LevelType LevelType;
public readonly string[] LocationTypeIdentifiers;
@@ -111,6 +113,7 @@ namespace Barotrauma
EventPrefabs = new List<Pair<EventPrefab, float>>();
ChildSets = new List<EventSet>();
BiomeIdentifier = element.GetAttributeString("biome", string.Empty);
MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0);
MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty);
@@ -1,5 +1,7 @@
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
@@ -8,38 +10,33 @@ namespace Barotrauma
{
private readonly XElement characterConfig;
private readonly List<Character> characters = new List<Character>();
protected readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
protected readonly HashSet<Character> requireKill = new HashSet<Character>();
protected readonly HashSet<Character> requireRescue = new HashSet<Character>();
private readonly string itemTag;
public override bool AllowRespawn => false;
private Item itemToDestroy;
protected bool wasDocked;
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations) :
base(prefab, locations)
{
characterConfig = prefab.ConfigElement.Element("Characters");
itemTag = prefab.ConfigElement.GetAttributeString("targetitem", "");
if (string.IsNullOrEmpty(itemTag))
{
DebugConsole.ThrowError($"Error in mission prefab \"{prefab.Identifier}\". Target item not defined.");
}
}
protected override void StartMissionSpecific(Level level)
{
itemToDestroy = null;
itemToDestroy = Item.ItemList.Find(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
if (itemToDestroy == null)
{
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".");
}
characters.Clear();
characterItems.Clear();
requireKill.Clear();
requireRescue.Clear();
if (!IsClient)
{
InitCharacters();
}
wasDocked = Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost);
}
private void InitCharacters()
@@ -57,48 +54,139 @@ namespace Barotrauma
foreach (XElement element in characterConfig.Elements())
{
string characterIdentifier = element.GetAttributeString("identifier", "");
string characterFrom = element.GetAttributeString("from", "");
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn character for abandoned outpost mission: character prefab \"" + characterIdentifier + "\" not found");
return;
}
if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null);
string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human,
moduleFlags ?? humanPrefab.GetModuleFlags(),
spawnPointTags ?? humanPrefab.GetSpawnPointTags());
if (spawnPos == null)
int defaultCount = element.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
defaultCount = element.GetAttributeInt("amount", 1);
}
int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255);
int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255);
int count = Rand.Range(min, max + 1);
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server);
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, spawnPos.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
{
string characterIdentifier = element.GetAttributeString("identifier", "");
string characterFrom = element.GetAttributeString("from", "");
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + characterIdentifier + "\" not found");
continue;
}
for (int i = 0; i < count; i++)
{
LoadHuman(humanPrefab, element, submarine);
}
}
else
{
string speciesName = element.GetAttributeString("character", element.GetAttributeString("identifier", ""));
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found");
continue;
}
for (int i = 0; i < count; i++)
{
LoadMonster(characterPrefab, element, submarine);
}
}
}
}
private void LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
{
string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null);
string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human,
moduleFlags ?? humanPrefab.GetModuleFlags(),
spawnPointTags ?? humanPrefab.GetSpawnPointTags(),
element.GetAttributeBool("asfaraspossible", false));
if (spawnPos == null)
{
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server);
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, spawnPos.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
if (element.GetAttributeBool("requirerescue", false))
{
requireRescue.Add(spawnedCharacter);
spawnedCharacter.TeamID = CharacterTeamType.FriendlyNPC;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
#endif
}
else
{
spawnedCharacter.TeamID = CharacterTeamType.None;
humanPrefab.InitializeCharacter(spawnedCharacter, spawnPos);
humanPrefab.GiveItems(spawnedCharacter, Submarine.MainSub, Rand.RandSync.Server, createNetworkEvents: false);
}
humanPrefab.InitializeCharacter(spawnedCharacter, spawnPos);
humanPrefab.GiveItems(spawnedCharacter, Submarine.MainSub, Rand.RandSync.Server, createNetworkEvents: false);
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(spawnedCharacter);
}
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
}
characters.Add(spawnedCharacter);
private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
{
string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null);
string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
if (spawnPos == null)
{
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
characters.Add(spawnedCharacter);
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(spawnedCharacter);
}
if (spawnedCharacter.Inventory != null)
{
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
}
}
public override void Update(float deltaTime)
{
if (State == 0 && itemToDestroy != null && itemToDestroy.Condition <= 0.0f)
switch (state)
{
State = 1;
case 0:
if (requireKill.All(c => c.Removed || c.IsDead) &&
requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player))
{
State = 1;
}
break;
#if SERVER
case 1:
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
{
if (!Submarine.MainSub.AtStartPosition || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)))
{
GameMain.Server.EndGame();
State = 2;
}
}
break;
#endif
}
}
public override void End()
{
completed = itemToDestroy == null || itemToDestroy.Condition <= 0.0f;
completed = State > 0;
if (completed)
{
if (Prefab.LocationTypeChangeOnCompleted != null)
@@ -43,21 +43,21 @@ namespace Barotrauma
public virtual string SuccessMessage
{
get { return successMessage; }
private set { successMessage = value; }
//private set { successMessage = value; }
}
private string failureMessage;
public virtual string FailureMessage
{
get { return failureMessage; }
private set { failureMessage = value; }
//private set { failureMessage = value; }
}
protected string description;
public virtual string Description
{
get { return description; }
private set { description = value; }
//private set { description = value; }
}
public int Reward
@@ -110,7 +110,7 @@ namespace Barotrauma
description = prefab.Description;
successMessage = prefab.SuccessMessage;
FailureMessage = prefab.FailureMessage;
failureMessage = prefab.FailureMessage;
Headers = new List<string>(prefab.Headers);
Messages = new List<string>(prefab.Messages);
@@ -118,12 +118,13 @@ namespace Barotrauma
for (int n = 0; n < 2; n++)
{
if (description != null) description = description.Replace("[location" + (n + 1) + "]", locations[n].Name);
if (successMessage != null) successMessage = successMessage.Replace("[location" + (n + 1) + "]", locations[n].Name);
if (failureMessage != null) failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locations[n].Name);
string locationName = $"‖color:gui.orange‖{locations[n].Name}‖end‖";
if (description != null) description = description.Replace("[location" + (n + 1) + "]", locationName);
if (successMessage != null) successMessage = successMessage.Replace("[location" + (n + 1) + "]", locationName);
if (failureMessage != null) failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locationName);
for (int m = 0; m < Messages.Count; m++)
{
Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locations[n].Name);
Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locationName);
}
}
if (description != null) description = description.Replace("[reward]", Reward.ToString("N0"));
@@ -180,7 +181,7 @@ namespace Barotrauma
{
foreach (string categoryToShow in Prefab.UnhideEntitySubCategories)
{
foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab.HasSubCategory(categoryToShow)))
foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab?.HasSubCategory(categoryToShow) ?? false))
{
entityToShow.HiddenInGame = false;
}
@@ -18,9 +18,10 @@ namespace Barotrauma
Nest = 0x10,
Mineral = 0x20,
Combat = 0x40,
AbandonedOutpost = 0x80,
OutpostDestroy = 0x80,
OutpostRescue = 0x100,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | AbandonedOutpost
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | OutpostDestroy | OutpostRescue
}
partial class MissionPrefab
@@ -35,7 +36,8 @@ namespace Barotrauma
{ MissionType.Beacon, typeof(BeaconMission) },
{ MissionType.Nest, typeof(NestMission) },
{ MissionType.Mineral, typeof(MineralMission) },
{ MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) },
{ MissionType.OutpostDestroy, typeof(OutpostDestroyMission) },
{ MissionType.OutpostRescue, typeof(AbandonedOutpostMission) },
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
{
@@ -166,7 +168,10 @@ namespace Barotrauma
FailureMessage = element.GetAttributeString("failuremessage", "");
}
SonarLabel = TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? element.GetAttributeString("sonarlabel", "");
SonarLabel =
TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ??
TextManager.Get("MissionSonarLabel." + element.GetAttributeString("sonarlabel", ""), true) ??
element.GetAttributeString("sonarlabel", "");
SonarIconIdentifier = element.GetAttributeString("sonaricon", "");
MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false);
@@ -8,7 +8,7 @@ namespace Barotrauma
partial class MonsterMission : Mission
{
//string = filename, point = min,max
private readonly HashSet<Tuple<CharacterPrefab, Point>> monsterPrefabs = new HashSet<Tuple<CharacterPrefab, Point>>();
private readonly HashSet<(CharacterPrefab character, Point amountRange)> monsterPrefabs = new HashSet<(CharacterPrefab character, Point amountRange)>();
private readonly List<Character> monsters = new List<Character>();
private readonly List<Vector2> sonarPositions = new List<Vector2>();
@@ -43,7 +43,7 @@ namespace Barotrauma
if (characterPrefab != null)
{
int monsterCount = Math.Min(prefab.ConfigElement.GetAttributeInt("monstercount", 1), 255);
monsterPrefabs.Add(new Tuple<CharacterPrefab, Point>(characterPrefab, new Point(monsterCount)));
monsterPrefabs.Add((characterPrefab, new Point(monsterCount)));
}
else
{
@@ -73,7 +73,7 @@ namespace Barotrauma
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab != null)
{
monsterPrefabs.Add(new Tuple<CharacterPrefab, Point>(characterPrefab, new Point(min, max)));
monsterPrefabs.Add((characterPrefab, new Point(min, max)));
}
else
{
@@ -83,7 +83,7 @@ namespace Barotrauma
if (monsterPrefabs.Any())
{
var characterParams = new CharacterParams(monsterPrefabs.First().Item1.FilePath);
var characterParams = new CharacterParams(monsterPrefabs.First().character.FilePath);
description = description.Replace("[monster]",
TextManager.Get("character." + characterParams.SpeciesTranslationOverride, returnNull: true) ??
TextManager.Get("character." + characterParams.SpeciesName));
@@ -115,12 +115,12 @@ namespace Barotrauma
if (!IsClient)
{
Level.Loaded.TryGetInterestingPosition(true, spawnPosType, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos);
foreach (var monster in monsterPrefabs)
foreach (var (character, amountRange) in monsterPrefabs)
{
int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1);
int amount = Rand.Range(amountRange.X, amountRange.Y + 1);
for (int i = 0; i < amount; i++)
{
monsters.Add(Character.Create(monster.Item1.Identifier, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false));
monsters.Add(Character.Create(character.Identifier, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false));
}
}
@@ -18,12 +18,14 @@ namespace Barotrauma
private Vector2? spawnPos;
private readonly bool disallowed;
private bool disallowed;
private readonly Level.PositionType spawnPosType;
private bool spawnPending;
private int maxAmountPerLevel = int.MaxValue;
public List<Character> Monsters => monsters;
public Vector2? SpawnPos => spawnPos;
public bool SpawnPending => spawnPending;
@@ -70,6 +72,8 @@ namespace Barotrauma
minAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount);
maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount);
maxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue);
var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPosTypeStr) ||
!Enum.TryParse(spawnPosTypeStr, true, out spawnPosType))
@@ -345,6 +349,15 @@ namespace Barotrauma
if (spawnPos == null)
{
if (maxAmountPerLevel < int.MaxValue)
{
if (Character.CharacterList.Count(c => c.SpeciesName == speciesName) >= maxAmountPerLevel)
{
disallowed = true;
return;
}
}
FindSpawnPosition(affectSubImmediately: true);
//the event gets marked as finished if a spawn point is not found
if (isFinished) { return; }
@@ -400,7 +413,7 @@ namespace Barotrauma
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (submarine.WorldPosition.Y > Level.Loaded.AbyssStart)
if (submarine.WorldPosition.Y > 0)
{
return;
}
@@ -242,6 +242,21 @@ namespace Barotrauma
conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f);
}
public void RenameCharacter(CharacterInfo characterInfo, string newName)
{
int identifier = characterInfo.GetIdentifierUsingOriginalName();
var match = characterInfos.FirstOrDefault(ci => ci.GetIdentifierUsingOriginalName() == identifier);
if (match == null)
{
DebugConsole.ThrowError($"Tried to rename an invalid crew member ({identifier})");
return;
}
match.Rename(newName);
RenameCharacterProjSpecific(match);
}
partial void RenameCharacterProjSpecific(CharacterInfo characterInfo);
public void FireCharacter(CharacterInfo characterInfo)
{
RemoveCharacterInfo(characterInfo);
@@ -277,6 +292,7 @@ namespace Barotrauma
private void UpdateConversations(float deltaTime)
{
if (GameMain.GameSession?.GameMode?.Preset == GameModePreset.TestMode) { return; }
if (GameMain.NetworkMember != null && GameMain.NetworkMember.ServerSettings.DisableBotConversations) { return; }
conversationTimer -= deltaTime;
@@ -305,19 +321,33 @@ namespace Barotrauma
{
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)
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode)
{
float normalizedReputation = MathUtils.InverseLerp(
campaignMode.Map.CurrentLocation.Reputation.MinReputation,
campaignMode.Map.CurrentLocation.Reputation.MaxReputation,
campaignMode.Map.CurrentLocation.Reputation.Value);
if (normalizedReputation < 0.2f)
if (campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false)
{
dialogFlags.Add("LowReputation");
if (npc.TeamID == CharacterTeamType.None)
{
dialogFlags.Add("Bandit");
}
else if (npc.TeamID == CharacterTeamType.FriendlyNPC)
{
dialogFlags.Add("Hostage");
}
}
else if (normalizedReputation > 0.8f)
else if (campaignMode.Map?.CurrentLocation?.Reputation != null)
{
dialogFlags.Add("HighReputation");
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));
@@ -5,9 +5,39 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
internal struct CampaignSettings
{
public static CampaignSettings Empty = new CampaignSettings();
// Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down
public static CampaignSettings Unsure = Empty;
public bool RadiationEnabled { get; set; }
public CampaignSettings(IReadMessage inc)
{
RadiationEnabled = inc.ReadBoolean();
}
public CampaignSettings(XElement element)
{
RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLower(), true);
}
public void Serialize(IWriteMessage msg)
{
msg.Write(RadiationEnabled);
}
public XElement Save()
{
return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLower(), RadiationEnabled));
}
}
abstract partial class CampaignMode : GameMode
{
const int MaxMoney = int.MaxValue / 2; //about 1 billion
@@ -31,6 +61,8 @@ namespace Barotrauma
protected XElement petsElement;
public CampaignSettings Settings;
private List<Mission> extraMissions = new List<Mission>();
public enum TransitionType
@@ -224,7 +256,7 @@ namespace Barotrauma
if (levelData.HasBeaconStation && !levelData.IsBeaconActive)
{
var beaconMissionPrefab = MissionPrefab.List.Find(m => m.Identifier.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase));
var beaconMissionPrefab = MissionPrefab.List.Find(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase)));
if (beaconMissionPrefab != null && !Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type))
{
extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations));
@@ -232,8 +264,12 @@ namespace Barotrauma
}
if (levelData.HasHuntingGrounds)
{
var huntingGroundsMissionPrefab = MissionPrefab.List.Find(m => m.Identifier.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase));
if (huntingGroundsMissionPrefab != null && !Missions.Any(m => m.Prefab.Type == huntingGroundsMissionPrefab.Type))
var huntingGroundsMissionPrefab = MissionPrefab.List.Find(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase)));
if (huntingGroundsMissionPrefab == null)
{
DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggroundsnoreward\" found.");
}
else if (!Missions.Any(m => m.Prefab.Type == huntingGroundsMissionPrefab.Type))
{
extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations));
}
@@ -342,7 +378,7 @@ namespace Barotrauma
nextLevel = map.StartLocation.LevelData;
return TransitionType.End;
}
if (Level.Loaded.EndLocation != null && Level.Loaded.EndLocation.HasOutpost() && Level.Loaded.EndOutpost != null)
if (Level.Loaded.EndLocation != null && Level.Loaded.EndLocation.Type.HasOutpost && Level.Loaded.EndOutpost != null)
{
nextLevel = Level.Loaded.EndLocation.LevelData;
return TransitionType.ProgressToNextLocation;
@@ -361,12 +397,12 @@ namespace Barotrauma
}
else if (leavingSub.AtStartPosition)
{
if (map.CurrentLocation.HasOutpost() && Level.Loaded.StartOutpost != null)
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.HasOutpost() &&
else if (map.SelectedLocation != null && map.SelectedLocation != map.CurrentLocation && !map.CurrentLocation.Type.HasOutpost &&
map.SelectedConnection != null && Level.Loaded.LevelData != map.SelectedConnection.LevelData)
{
nextLevel = map.SelectedConnection.LevelData;
@@ -584,14 +620,12 @@ namespace Barotrauma
public bool TryHireCharacter(Location location, CharacterInfo characterInfo)
{
if (characterInfo == null) { return false; }
if (Money < characterInfo.Salary) { return false; }
characterInfo.IsNewHire = true;
location.RemoveHireableCharacter(characterInfo);
CrewManager.AddCharacterInfo(characterInfo);
Money -= characterInfo.Salary;
return true;
}
@@ -59,13 +59,14 @@ namespace Barotrauma
InitCampaignData();
}
public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub)
public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings)
{
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.map = new Map(campaign, mapSeed, settings);
campaign.Settings = settings;
}
campaign.InitProjSpecific();
return campaign;
@@ -128,11 +129,14 @@ namespace Barotrauma
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "campaignsettings":
Settings = new CampaignSettings(subElement);
break;
case "map":
if (map == null)
{
//map not created yet, loading this campaign for the first time
map = Map.Load(this, subElement);
map = Map.Load(this, subElement, Settings);
}
else
{
@@ -103,12 +103,12 @@ namespace Barotrauma
/// <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)
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, CampaignSettings settings, string seed = null, MissionType missionType = MissionType.None)
: this(submarineInfo)
{
this.SavePath = savePath;
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionType: missionType);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, settings, missionType: missionType);
}
/// <summary>
@@ -118,14 +118,13 @@ namespace Barotrauma
: this(submarineInfo)
{
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionPrefabs: missionPrefabs);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, CampaignSettings.Empty, missionPrefabs: missionPrefabs);
}
/// <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)
public GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines, XDocument doc, string saveFile) : this(submarineInfo, ownedSubmarines)
{
this.SavePath = saveFile;
GameMain.GameSession = this;
@@ -159,7 +158,7 @@ namespace Barotrauma
}
}
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, IEnumerable<MissionPrefab> missionPrefabs = null, MissionType missionType = MissionType.None)
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable<MissionPrefab> missionPrefabs = null, MissionType missionType = MissionType.None)
{
if (gameModePreset.GameModeType == typeof(CoOpMode))
{
@@ -175,7 +174,7 @@ namespace Barotrauma
}
else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
{
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub);
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
if (campaign != null && selectedSub != null)
{
campaign.Money = Math.Max(MultiPlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price);
@@ -185,7 +184,7 @@ namespace Barotrauma
#if CLIENT
else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
{
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub);
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
if (campaign != null && selectedSub != null)
{
campaign.Money = Math.Max(SinglePlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price);
@@ -400,6 +399,8 @@ namespace Barotrauma
}
GUI.PreventPauseMenuToggle = false;
HintManager.OnRoundStarted();
#endif
}
@@ -467,7 +468,7 @@ namespace Barotrauma
{
mpCampaign.CargoManager.CreatePurchasedItems();
#if SERVER
mpCampaign.SendCrewState(false, null);
mpCampaign.SendCrewState(null, default, null);
#endif
}
mpCampaign.UpgradeManager.ApplyUpgrades();
@@ -544,7 +545,7 @@ namespace Barotrauma
{
Submarine.SetPosition(spawnPos);
myPort.Dock(outPostPort);
myPort.Lock(true, forcePosition: true, applyEffects: false);
myPort.Lock(isNetworkMessage: true, applyEffects: false);
}
else
{
@@ -583,9 +584,10 @@ namespace Barotrauma
{
EventManager?.Update(deltaTime);
GameMode?.Update(deltaTime);
foreach (Mission mission in missions)
//backwards for loop because the missions may get completed and removed from the list in Update()
for (int i = missions.Count - 1; i >= 0; i--)
{
mission.Update(deltaTime);
missions[i].Update(deltaTime);
}
UpdateProjSpecific(deltaTime);
}
@@ -636,6 +638,10 @@ namespace Barotrauma
StatusEffect.StopAll();
missions.Clear();
IsRunning = false;
#if CLIENT
HintManager.OnRoundEnded();
#endif
}
public void KillCharacter(Character character)
@@ -1,5 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -39,5 +39,12 @@ namespace Barotrauma
AvailableCharacters.ForEach(c => c.Remove());
AvailableCharacters.Clear();
}
public void RenameCharacter(CharacterInfo characterInfo, string newName)
{
if (characterInfo == null || string.IsNullOrEmpty(newName)) { return; }
AvailableCharacters.FirstOrDefault(ci => ci == characterInfo)?.Rename(newName);
PendingHires.FirstOrDefault(ci => ci == characterInfo)?.Rename(newName);
}
}
}
@@ -171,7 +171,7 @@ namespace Barotrauma
/// <summary>
/// How many corpses there can be in a sub before they start to get despawned
/// </summary>
public int CorpsesPerSubDespawnThreshold { get; set; } = 5;
public int CorpsesPerSubDespawnThreshold { get; set; } = 10;
private string overrideSaveFolder, overrideMultiplayerSaveFolder;
@@ -301,10 +301,14 @@ namespace Barotrauma
public volatile bool WaitingForAutoUpdate;
public bool DisableInGameHints { get; set; }
#if DEBUG
public bool AutomaticQuickStartEnabled { get; set; }
public bool AutomaticCampaignLoadEnabled { get; set; }
public bool TextManagerDebugModeEnabled { get; set; }
public bool ModBreakerMode { get; set; }
#endif
private System.IO.FileSystemWatcher modsFolderWatcher;
@@ -735,6 +739,10 @@ namespace Barotrauma
private bool textScaleDirty;
public List<string> CompletedTutorialNames { get; private set; }
/// <summary>
/// Identifiers of hints the player has chosen not to see again
/// </summary>
public HashSet<string> IgnoredHints { get; private set; } = new HashSet<string>();
public HashSet<string> EncounteredCreatures { get; private set; } = new HashSet<string>();
public HashSet<string> KilledCreatures { get; private set; } = new HashSet<string>();
@@ -1151,6 +1159,12 @@ namespace Barotrauma
CompletedTutorialNames.Add(element.GetAttributeString("name", ""));
}
}
if (doc.Root.Element("ignoredhints") is XElement ignoredHintsElement)
{
IgnoredHints = new HashSet<string>(ignoredHintsElement.GetAttributeStringArray("identifiers", new string[0], convertToLowerInvariant: true));
}
XElement encounters = doc.Root.Element("encountered");
if (encounters != null)
{
@@ -1172,7 +1186,7 @@ namespace Barotrauma
#endregion
#region Save PlayerConfig
public void SaveNewPlayerConfig()
public bool SaveNewPlayerConfig()
{
XDocument doc = new XDocument();
UnsavedSettings = false;
@@ -1211,11 +1225,13 @@ namespace Barotrauma
new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning),
new XAttribute("corpsedespawndelay", CorpseDespawnDelay),
new XAttribute("corpsespersubdespawnthreshold", CorpsesPerSubDespawnThreshold),
new XAttribute("usedualmodesockets", UseDualModeSockets)
new XAttribute("usedualmodesockets", UseDualModeSockets),
new XAttribute("disableingamehints", DisableInGameHints)
#if DEBUG
, new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled)
, new XAttribute("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled)
, new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled)
, new XAttribute("modbreakermode", ModBreakerMode)
#endif
);
@@ -1403,6 +1419,8 @@ namespace Barotrauma
}
doc.Root.Add(tutorialElement);
doc.Root.Add(new XElement("ignoredhints", new XAttribute("identifiers", string.Join(",", IgnoredHints).Trim().ToLowerInvariant())));
doc.Root.Add(new XElement("encountered", new XAttribute("creatures", string.Join(",", EncounteredCreatures).Trim().ToLowerInvariant())));
doc.Root.Add(new XElement("killed", new XAttribute("creatures", string.Join(",", KilledCreatures).Trim().ToLowerInvariant())));
@@ -1426,7 +1444,10 @@ namespace Barotrauma
DebugConsole.ThrowError("Saving game settings failed.", e);
GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace());
return false;
}
return true;
}
#endregion
@@ -1460,10 +1481,12 @@ namespace Barotrauma
EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown);
ShowTutorialSkipWarning = doc.Root.GetAttributeBool("tutorialskipwarning", true);
UseDualModeSockets = doc.Root.GetAttributeBool("usedualmodesockets", true);
DisableInGameHints = doc.Root.GetAttributeBool("disableingamehints", DisableInGameHints);
#if DEBUG
AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled);
AutomaticCampaignLoadEnabled = doc.Root.GetAttributeBool("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled);
TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled);
ModBreakerMode = doc.Root.GetAttributeBool("modbreakermode", ModBreakerMode);
#endif
XElement gameplayElement = doc.Root.Element("gameplay");
jobPreferences = new List<Pair<string, int>>();
@@ -1579,6 +1602,32 @@ namespace Barotrauma
CurrentCorePackage = null;
enabledRegularPackages.Clear();
#if DEBUG && CLIENT
if (ModBreakerMode)
{
CurrentCorePackage = ContentPackage.CorePackages.GetRandom();
foreach (var regularPackage in ContentPackage.RegularPackages)
{
if (Rand.Range(0.0, 1.0) <= 0.5)
{
enabledRegularPackages.Add(regularPackage);
}
}
ContentPackage.SortContentPackages(p =>
{
return Rand.Int(int.MaxValue);
}, config: this);
if (CurrentCorePackage == null)
{
CurrentCorePackage = ContentPackage.CorePackages.First();
}
TextManager.LoadTextPacks(AllEnabledPackages);
return;
}
#endif
var contentPackagesElement = doc.Root.Element("contentpackages");
if (contentPackagesElement != null)
{
@@ -1725,6 +1774,7 @@ namespace Barotrauma
AutoUpdateWorkshopItems = true;
TextScale = 1;
textScaleDirty = false;
DisableInGameHints = false;
}
}
}
@@ -154,7 +154,7 @@ namespace Barotrauma.Items.Components
var prevDockingTarget = DockingTarget;
Undock(applyEffects: false);
Dock(prevDockingTarget);
Lock(true, applyEffects: false);
Lock(isNetworkMessage: true, applyEffects: false);
}
}
@@ -240,7 +240,7 @@ namespace Barotrauma.Items.Components
}
public void Lock(bool isNetworkMessage, bool forcePosition = false, bool applyEffects = true)
public void Lock(bool isNetworkMessage, bool applyEffects = true)
{
#if CLIENT
if (GameMain.Client != null && !isNetworkMessage) { return; }
@@ -262,20 +262,17 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnUse, 1.0f);
}
if (forcePosition)
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass ||
DockingTarget.item.Submarine.Info.IsOutpost)
{
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass ||
DockingTarget.item.Submarine.Info.IsOutpost)
{
item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff));
}
else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass ||
item.Submarine.Info.IsOutpost)
{
DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
}
item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff));
}
else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass ||
item.Submarine.Info.IsOutpost)
{
DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
}
ConnectWireBetweenPorts();
CreateJoint(true);
@@ -988,7 +985,7 @@ namespace Barotrauma.Items.Components
}
else
{
Lock(isNetworkMessage: false, forcePosition: true);
Lock(isNetworkMessage: false);
}
}
else
@@ -305,9 +305,21 @@ namespace Barotrauma.Items.Components
private void ToggleState(ActionType actionType, Character user)
{
if (toggleCooldownTimer > 0.0f && user != lastUser) { OnFailedToOpen(); return; }
if (toggleCooldownTimer > 0.0f && user != lastUser)
{
OnFailedToOpen();
return;
}
toggleCooldownTimer = ToggleCoolDown;
if (IsStuck || IsJammed) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
if (IsStuck || IsJammed)
{
#if CLIENT
if (IsStuck) { HintManager.OnTryOpenStuckDoor(user); }
#endif
toggleCooldownTimer = 1.0f;
OnFailedToOpen();
return;
}
lastUser = user;
SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked);
}
@@ -12,7 +12,8 @@ namespace Barotrauma.Items.Components
{
partial class RangedWeapon : ItemComponent
{
private float reload, reloadTimer;
private float reload;
public float ReloadTimer { get; private set; }
private Vector2 barrelPos;
@@ -75,17 +76,17 @@ namespace Barotrauma.Items.Components
public override void Equip(Character character)
{
reloadTimer = Math.Min(reload, 1.0f);
ReloadTimer = Math.Min(reload, 1.0f);
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
reloadTimer -= deltaTime;
ReloadTimer -= deltaTime;
if (reloadTimer < 0.0f)
if (ReloadTimer < 0.0f)
{
reloadTimer = 0.0f;
ReloadTimer = 0.0f;
IsActive = false;
}
}
@@ -101,10 +102,10 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character.Removed) { return false; }
if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || reloadTimer > 0.0f) { return false; }
if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || ReloadTimer > 0.0f) { return false; }
IsActive = true;
reloadTimer = reload;
ReloadTimer = reload;
if (item.AiTarget != null)
{
@@ -854,9 +854,11 @@ namespace Barotrauma.Items.Components
object value = property.GetValue(target);
if (door.Stuck > 0)
{
bool isCutting = effect.propertyEffects[i].GetType() == typeof(float) && (float)effect.propertyEffects[i] < 0;
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White,
effect.propertyEffects[i].GetType() == typeof(float) && (float)effect.propertyEffects[i] < 0 ? "progressbar.cutting" : "progressbar.welding");
textTag: isCutting ? "progressbar.cutting" : "progressbar.welding");
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
if (!isCutting) { HintManager.OnWeldingDoor(user); }
}
}
}
@@ -771,6 +771,10 @@ namespace Barotrauma.Items.Components
brokenEffects.ForEach(e => e.SetUser(user));
}
}
#if CLIENT
HintManager.OnStatusEffectApplied(this, type, character);
#endif
}
public virtual void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap)
@@ -212,6 +212,7 @@ namespace Barotrauma.Items.Components
progressState = 0.0f;
timeUntilReady = 0.0f;
UpdateRequiredTimeProjSpecific();
inputContainer.Inventory.Locked = false;
outputContainer.Inventory.Locked = false;
@@ -279,6 +280,7 @@ namespace Barotrauma.Items.Components
if (powerConsumption <= 0) { Voltage = 1.0f; }
timeUntilReady -= deltaTime * Math.Min(Voltage, 1.0f);
UpdateRequiredTimeProjSpecific();
if (timeUntilReady > 0.0f) { return; }
@@ -360,6 +362,8 @@ namespace Barotrauma.Items.Components
}
}
partial void UpdateRequiredTimeProjSpecific();
private bool CanBeFabricated(FabricationRecipe fabricableItem)
{
if (fabricableItem == null) { return false; }
@@ -221,6 +221,13 @@ namespace Barotrauma.Items.Components
}
}
#if CLIENT
if(PowerOn && AvailableFuel < 1)
{
HintManager.OnReactorOutOfFuel(this);
}
#endif
prevAvailableFuel = AvailableFuel;
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
@@ -150,7 +150,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
currPowerConsumption = powerConsumption;
currPowerConsumption = (currentMode == Mode.Active) ? powerConsumption : powerConsumption * 0.1f;
UpdateOnActiveEffects(deltaTime);
@@ -22,6 +22,11 @@ namespace Barotrauma.Items.Components
private const float AutoPilotMaxSpeed = 0.5f;
private const float AIPilotMaxSpeed = 1.0f;
/// <summary>
/// How fast the steering vector adjusts when the nav terminal is operated by something else than a character (= signals)
/// </summary>
const float DefaultSteeringAdjustSpeed = 0.2f;
private Vector2 targetVelocity;
private Vector2 steeringInput;
@@ -543,6 +548,10 @@ namespace Barotrauma.Items.Components
{
TargetVelocity *= 100.0f / velMagnitude;
}
#if CLIENT
HintManager.OnAutoPilotPathUpdated(this);
#endif
}
private float? GetNodePenalty(PathNode node, PathNode nextNode)
@@ -700,7 +709,10 @@ namespace Barotrauma.Items.Components
{
if (connection.Name == "velocity_in")
{
TargetVelocity = XMLExtensions.ParseVector2(signal, errorMessages: false);
steeringAdjustSpeed = DefaultSteeringAdjustSpeed;
steeringInput = XMLExtensions.ParseVector2(signal, errorMessages: false);
steeringInput.X = MathHelper.Clamp(steeringInput.X, -100.0f, 100.0f);
steeringInput.Y = MathHelper.Clamp(steeringInput.Y, -100.0f, 100.0f);
}
else
{
@@ -4,7 +4,7 @@ using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class AndComponent : ItemComponent
{
{
protected string output, falseOutput;
//an array to keep track of how long ago a non-zero signal was received on both inputs
@@ -27,14 +27,41 @@ namespace Barotrauma.Items.Components
public string Output
{
get { return output; }
set { output = value; }
set
{
if (value == null) { return; }
output = value;
if (output.Length > MaxOutputLength)
{
output = output.Substring(0, MaxOutputLength);
}
}
}
[InGameEditable, Serialize("", true, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)]
public string FalseOutput
{
get { return falseOutput; }
set { falseOutput = value; }
set
{
if (value == null) { return; }
falseOutput = value;
if (falseOutput.Length > MaxOutputLength)
{
falseOutput = falseOutput.Substring(0, MaxOutputLength);
}
}
}
private int maxOutputLength;
[Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")]
public int MaxOutputLength
{
get { return maxOutputLength; }
set
{
maxOutputLength = Math.Max(value, 0);
}
}
public AndComponent(Item item, XElement element)
@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
@@ -10,6 +11,9 @@ namespace Barotrauma.Items.Components
private string output = "0,0,0,0";
[InGameEditable, Serialize(false, true, description: "When enabled makes the component translate the signal from HSV into RGB where red is the hue between 0 and 360, green is the saturation between 0 and 1 and blue is the value between 0 and 1.", alwaysUseInstanceValues: true)]
public bool UseHSV { get; set; }
public ColorComponent(Item item, XElement element)
: base(item, element)
{
@@ -24,10 +28,23 @@ namespace Barotrauma.Items.Components
private void UpdateOutput()
{
output = receivedSignal[0].ToString("G", CultureInfo.InvariantCulture);
output += "," + receivedSignal[1].ToString("G", CultureInfo.InvariantCulture);
output += "," + receivedSignal[2].ToString("G", CultureInfo.InvariantCulture);
output += "," + receivedSignal[3].ToString("G", CultureInfo.InvariantCulture);
float signalR = receivedSignal[0],
signalG = receivedSignal[1],
signalB = receivedSignal[2],
signalA = receivedSignal[3];
if (UseHSV)
{
Color hsvColor = ToolBox.HSVToRGB(signalR, signalG, signalB);
signalR = hsvColor.R / (float) byte.MaxValue;
signalG = hsvColor.G / (float) byte.MaxValue;
signalB = hsvColor.B / (float) byte.MaxValue;
}
output = signalR.ToString("G", CultureInfo.InvariantCulture);
output += "," + signalG.ToString("G", CultureInfo.InvariantCulture);
output += "," + signalB.ToString("G", CultureInfo.InvariantCulture);
output += "," + signalA.ToString("G", CultureInfo.InvariantCulture);
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
@@ -15,18 +15,45 @@ namespace Barotrauma.Items.Components
//the output is sent if both inputs have received a signal within the timeframe
protected float timeFrame;
[InGameEditable, Serialize("1", true, description: "The signal this item outputs when the condition is met.", alwaysUseInstanceValues: true)]
[InGameEditable, Serialize("1", true, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)]
public string Output
{
get { return output; }
set { output = value; }
set
{
if (value == null) { return; }
output = value;
if (output.Length > MaxOutputLength)
{
output = output.Substring(0, MaxOutputLength);
}
}
}
[InGameEditable, Serialize("", true, description: "The signal this item outputs when the condition is not met.", alwaysUseInstanceValues: true)]
[InGameEditable, Serialize("", true, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)]
public string FalseOutput
{
get { return falseOutput; }
set { falseOutput = value; }
set
{
if (value == null) { return; }
falseOutput = value;
if (falseOutput.Length > MaxOutputLength)
{
falseOutput = falseOutput.Substring(0, MaxOutputLength);
}
}
}
private int maxOutputLength;
[Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")]
public int MaxOutputLength
{
get { return maxOutputLength; }
set
{
maxOutputLength = Math.Max(value, 0);
}
}
[InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.", alwaysUseInstanceValues: true)]
@@ -74,11 +74,48 @@ namespace Barotrauma.Items.Components
}
}
private string output;
[InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)]
public string Output { get; set; }
public string Output
{
get { return output; }
set
{
if (value == null) { return; }
output = value;
if (output.Length > MaxOutputLength)
{
output = output.Substring(0, MaxOutputLength);
}
}
}
private string falseOutput;
[InGameEditable, Serialize("", true, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)]
public string FalseOutput { get; set; }
public string FalseOutput
{
get { return falseOutput; }
set
{
if (value == null) { return; }
falseOutput = value;
if (falseOutput.Length > MaxOutputLength)
{
falseOutput = falseOutput.Substring(0, MaxOutputLength);
}
}
}
private int maxOutputLength;
[Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")]
public int MaxOutputLength
{
get { return maxOutputLength; }
set
{
maxOutputLength = Math.Max(value, 0);
}
}
[Editable(DecimalCount = 3), Serialize(0.01f, true, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)]
public float MinimumVelocity
@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using System;
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -17,8 +18,22 @@ namespace Barotrauma.Items.Components
private bool nonContinuousOutputSent;
private string output;
[InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.", alwaysUseInstanceValues: true)]
public string Output { get; set; }
public string Output
{
get { return output; }
set
{
if (value == null) { return; }
output = value;
if (output.Length > MaxOutputLength)
{
output = output.Substring(0, MaxOutputLength);
}
}
}
[InGameEditable, Serialize(false, true, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)]
public bool UseCaptureGroup { get; set; }
@@ -52,6 +67,17 @@ namespace Barotrauma.Items.Components
}
}
private int maxOutputLength;
[Editable, Serialize(200, false, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking issues.")]
public int MaxOutputLength
{
get { return maxOutputLength; }
set
{
maxOutputLength = Math.Max(value, 0);
}
}
public RegExFindComponent(Item item, XElement element)
: base(item, element)
{
@@ -1,17 +1,56 @@
using System.Xml.Linq;
using System;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class SignalCheckComponent : ItemComponent
{
private string output;
[InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the target signal.", alwaysUseInstanceValues: true)]
public string Output { get; set; }
public string Output
{
get { return output; }
set
{
if (value == null) { return; }
output = value;
if (output.Length > MaxOutputLength)
{
output = output.Substring(0, MaxOutputLength);
}
}
}
private string falseOutput;
[InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the target signal.", alwaysUseInstanceValues: true)]
public string FalseOutput { get; set; }
public string FalseOutput
{
get { return falseOutput; }
set
{
if (value == null) { return; }
falseOutput = value;
if (falseOutput.Length > MaxOutputLength)
{
falseOutput = falseOutput.Substring(0, MaxOutputLength);
}
}
}
[InGameEditable, Serialize("", true, description: "The value to compare the received signals against.", alwaysUseInstanceValues: true)]
public string TargetSignal { get; set; }
private int maxOutputLength;
[Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")]
public int MaxOutputLength
{
get { return maxOutputLength; }
set
{
maxOutputLength = Math.Max(value, 0);
}
}
public SignalCheckComponent(Item item, XElement element)
: base(item, element)
{
@@ -1,4 +1,5 @@
using System.Xml.Linq;
using System;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -9,11 +10,48 @@ namespace Barotrauma.Items.Components
private bool fireInRange;
[InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)]
public string Output { get; set; }
private string output;
[InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected a fire.", alwaysUseInstanceValues: true)]
public string Output
{
get { return output; }
set
{
if (value == null) { return; }
output = value;
if (output.Length > MaxOutputLength)
{
output = output.Substring(0, MaxOutputLength);
}
}
}
[InGameEditable, Serialize("0", true, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)]
public string FalseOutput { get; set; }
private string falseOutput;
[InGameEditable, Serialize("0", true, description: "The signal the item outputs when it has not detected a fire.", alwaysUseInstanceValues: true)]
public string FalseOutput
{
get { return falseOutput; }
set
{
if (value == null) { return; }
falseOutput = value;
if (falseOutput.Length > MaxOutputLength)
{
falseOutput = falseOutput.Substring(0, MaxOutputLength);
}
}
}
private int maxOutputLength;
[Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")]
public int MaxOutputLength
{
get { return maxOutputLength; }
set
{
maxOutputLength = Math.Max(value, 0);
}
}
public SmokeDetector(Item item, XElement element)
: base(item, element)
@@ -12,11 +12,48 @@ namespace Barotrauma.Items.Components
private bool isInWater;
private float stateSwitchDelay;
private string output;
[InGameEditable, Serialize("1", true, description: "The signal the item sends out when it's underwater.", alwaysUseInstanceValues: true)]
public string Output { get; set; }
public string Output
{
get { return output; }
set
{
if (value == null) { return; }
output = value;
if (output.Length > MaxOutputLength)
{
output = output.Substring(0, MaxOutputLength);
}
}
}
private string falseOutput;
[InGameEditable, Serialize("0", true, description: "The signal the item sends out when it's not underwater.", alwaysUseInstanceValues: true)]
public string FalseOutput { get; set; }
public string FalseOutput
{
get { return falseOutput; }
set
{
if (value == null) { return; }
falseOutput = value;
if (falseOutput.Length > MaxOutputLength)
{
falseOutput = falseOutput.Substring(0, MaxOutputLength);
}
}
}
private int maxOutputLength;
[Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")]
public int MaxOutputLength
{
get { return maxOutputLength; }
set
{
maxOutputLength = Math.Max(value, 0);
}
}
public WaterDetector(Item item, XElement element)
: base(item, element)
@@ -767,7 +767,6 @@ namespace Barotrauma.Items.Components
TryLaunch(deltaTime, ignorePower: true);
}
private bool outOfAmmo;
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget &&
@@ -834,7 +833,7 @@ namespace Barotrauma.Items.Components
}
if (container == null || container.ContainableItems.Count == 0)
{
if (!outOfAmmo && character.IsOnPlayerTeam)
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f);
}
@@ -850,7 +849,7 @@ namespace Barotrauma.Items.Components
}
loadItemsObjective.Abandoned += CheckRemainingAmmo;
loadItemsObjective.Completed += CheckRemainingAmmo;
return outOfAmmo;
return false;
void CheckRemainingAmmo()
{
@@ -1203,8 +1202,7 @@ namespace Barotrauma.Items.Components
resetUserTimer = 10.0f;
break;
case "trigger_in":
if (signal == "0") { return; }
lightComponent.IsOn = !lightComponent.IsOn;
if (signal == "0") { return; }
item.Use((float)Timing.Step, sender);
user = sender;
resetUserTimer = 10.0f;
@@ -826,7 +826,8 @@ namespace Barotrauma
{
slots[index].Add(item);
item.ParentInventory = this;
if (item.body != null)
bool equipped = (this as CharacterInventory)?.Owner is Character character && character.HasEquippedItem(item);
if (item.body != null && !equipped)
{
item.body.Enabled = false;
item.body.BodyType = FarseerPhysics.BodyType.Dynamic;
@@ -485,12 +485,14 @@ namespace Barotrauma
public int ClusterQuantity { get; }
public int ClusterSize { get; }
public bool IsIslandSpecifc { get; }
public bool AllowAtStart { get; }
public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific)
public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific, bool allowAtStart)
{
ClusterQuantity = clusterQuantity;
ClusterSize = clusterSize;
IsIslandSpecifc = isIslandSpecific;
AllowAtStart = allowAtStart;
}
}
@@ -965,7 +967,8 @@ namespace Barotrauma
LevelQuantity.Add(levelName, new FixedQuantityResourceInfo(
levelCommonnessElement.GetAttributeInt("clusterquantity", 0),
levelCommonnessElement.GetAttributeInt("clustersize", 0),
levelCommonnessElement.GetAttributeBool("isislandspecific", false)));
levelCommonnessElement.GetAttributeBool("isislandspecific", false),
levelCommonnessElement.GetAttributeBool("allowatstart", true)));
}
}
}
@@ -159,6 +159,11 @@ namespace Barotrauma
get { return AbyssArea.Y + AbyssArea.Height; }
}
public int AbyssEnd
{
get { return AbyssArea.Y; }
}
public class AbyssIsland
{
public readonly Rectangle Area;
@@ -1370,15 +1375,15 @@ namespace Barotrauma
float seaFloorPos = GetBottomPosition(xPos).Y;
//above the bottom of the level = can't place a point here
if (seaFloorPos > AbyssArea.Bottom) { continue; }
if (seaFloorPos > AbyssStart) { continue; }
float yPos = Rand.Range(Math.Max(seaFloorPos, AbyssArea.Y), AbyssArea.Bottom);
float yPos = MathHelper.Lerp(AbyssStart, Math.Max(seaFloorPos, AbyssArea.Y), Rand.Range(0.2f, 1.0f, Rand.RandSync.Server));
foreach (var abyssIsland in AbyssIslands)
{
if (abyssIsland.Area.Contains(new Point((int)xPos, (int)yPos)))
{
xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f);
xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1, Rand.RandSync.Server) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f);
}
}
@@ -2035,6 +2040,7 @@ namespace Barotrauma
{
if (l.Cell == null || l.Edge == null) { return false; }
if (resourceInfo.IsIslandSpecifc && !l.Cell.Island) { return false; }
if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > StartPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; }
if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; }
return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _);
@@ -3240,13 +3246,13 @@ namespace Barotrauma
}
#endif
}
if (StartLocation != null && !StartLocation.HasOutpost()) { continue; }
if (StartLocation != null && !StartLocation.Type.HasOutpost) { continue; }
}
else
{
//don't create an end outpost for locations
if (LevelData.Type == LevelData.LevelType.Outpost) { continue; }
if (EndLocation != null && !EndLocation.HasOutpost()) { continue; }
if (EndLocation != null && !EndLocation.Type.HasOutpost) { continue; }
}
SubmarineInfo outpostInfo;
@@ -3266,7 +3272,7 @@ namespace Barotrauma
{
var suitableParams = OutpostGenerationParams.Params
.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
if (suitableParams.Count() == 0)
if (!suitableParams.Any())
{
suitableParams = OutpostGenerationParams.Params
.Where(p => location == null || !p.AllowedLocationTypes.Any());
@@ -3305,7 +3311,7 @@ namespace Barotrauma
foreach (string categoryToHide in locationType.HideEntitySubcategories)
{
foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && me.prefab.HasSubCategory(categoryToHide)))
foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && (me.prefab?.HasSubCategory(categoryToHide) ?? false)))
{
entityToHide.HiddenInGame = true;
}
@@ -3444,7 +3450,12 @@ namespace Barotrauma
Item reactorItem = beaconItems.Find(it => it.GetComponent<Reactor>() != null);
Reactor reactorComponent = reactorItem.GetComponent<Reactor>();
ItemContainer reactorContainer = reactorItem.GetComponent<ItemContainer>();
Repairable repairable = reactorItem.GetComponent<Repairable>();
reactorComponent.FuelConsumptionRate = 0.0f;
if (repairable != null)
{
repairable.DeteriorationSpeed = 0.0f;
}
if (LevelData.IsBeaconActive)
{
if (reactorContainer.Inventory.IsEmpty())
@@ -236,6 +236,11 @@ namespace Barotrauma
DebugConsole.ThrowError("Failed to load a linked submarine (empty XML element). The save file may be corrupted.");
return;
}
if (!info.SubmarineElement.Elements().Any(e => e.Name.ToString().Equals("hull", StringComparison.OrdinalIgnoreCase)))
{
DebugConsole.ThrowError("Failed to load a linked submarine (the submarine contains no hulls).");
return;
}
IdRemap parentRemap = new IdRemap(Submarine.Info.SubmarineElement, Submarine.IdOffset);
sub = Submarine.Load(info, false, parentRemap);
@@ -322,7 +327,7 @@ namespace Barotrauma
sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset);
myPort.Dock(linkedPort);
myPort.Lock(true, applyEffects: false);
myPort.Lock(isNetworkMessage: true, applyEffects: false);
}
}
@@ -579,6 +579,16 @@ namespace Barotrauma
return false;
}
public LocationType GetLocationType()
{
if (IsCriticallyRadiated() && LocationType.List.FirstOrDefault(lt => lt.Identifier.Equals(Type.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newLocationType)
{
return newLocationType;
}
return Type;
}
public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
{
System.Diagnostics.Debug.Assert(Connections.Contains(connection));
@@ -14,6 +14,8 @@ namespace Barotrauma
public bool Passed;
public bool Locked;
public LevelData LevelData { get; set; }
public Vector2 CenterPos
@@ -54,8 +54,11 @@ namespace Barotrauma
get;
private set;
}
public string ReplaceInRadiation { get; }
public Sprite Sprite { get; private set; }
public Sprite RadiationSprite { get; }
public Color SpriteColor
{
@@ -85,6 +88,8 @@ namespace Barotrauma
HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", new string[0]).ToList();
ReplaceInRadiation = element.GetAttributeString(nameof(ReplaceInRadiation).ToLower(), "");
string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt");
try
{
@@ -140,6 +145,9 @@ namespace Barotrauma
Sprite = new Sprite(subElement, lazyLoad: true);
SpriteColor = subElement.GetAttributeColor("color", Color.White);
break;
case "radiationsymbol":
RadiationSprite = new Sprite(subElement, lazyLoad: true);
break;
case "changeto":
CanChangeTo.Add(new LocationTypeChange(Identifier, subElement, requireChangeMessages: true));
break;
@@ -59,18 +59,21 @@ namespace Barotrauma
public Radiation Radiation;
public Map()
public Map(CampaignSettings settings)
{
generationParams = MapGenerationParams.Instance;
Locations = new List<Location>();
Connections = new List<LocationConnection>();
Radiation = new Radiation(this, generationParams.RadiationParams);
Radiation = new Radiation(this, generationParams.RadiationParams)
{
Enabled = settings.RadiationEnabled
};
}
/// <summary>
/// Load a previously saved campaign map from XML
/// </summary>
private Map(CampaignMode campaign, XElement element) : this()
private Map(CampaignMode campaign, XElement element, CampaignSettings settings) : this(settings)
{
Seed = element.GetAttributeString("seed", "a");
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
@@ -87,7 +90,10 @@ namespace Barotrauma
Locations[i] = new Location(subElement);
break;
case "radiation":
Radiation = new Radiation(this, generationParams.RadiationParams, subElement);
Radiation = new Radiation(this, generationParams.RadiationParams, subElement)
{
Enabled = settings.RadiationEnabled
};
break;
}
}
@@ -107,6 +113,7 @@ namespace Barotrauma
var connection = new LocationConnection(Locations[locationIndices.X], Locations[locationIndices.Y])
{
Passed = subElement.GetAttributeBool("passed", false),
Locked = subElement.GetAttributeBool("locked", false),
Difficulty = subElement.GetAttributeFloat("difficulty", 0.0f)
};
Locations[locationIndices.X].Connections.Add(connection);
@@ -162,7 +169,7 @@ namespace Barotrauma
/// <summary>
/// Generate a new campaign map from the seed
/// </summary>
public Map(CampaignMode campaign, string seed) : this()
public Map(CampaignMode campaign, string seed, CampaignSettings settings) : this(settings)
{
Seed = seed;
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
@@ -404,6 +411,7 @@ namespace Barotrauma
leftMostLocation.ChangeType(LocationType.List.First(lt => lt.HasOutpost));
}
leftMostLocation.IsGateBetweenBiomes = true;
Connections[i].Locked = true;
}
}
@@ -688,6 +696,10 @@ namespace Barotrauma
SelectedConnection =
Connections.Find(c => c.Locations.Contains(GameMain.GameSession?.Campaign?.CurrentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ??
Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
if (SelectedConnection?.Locked ?? false)
{
DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
}
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
}
@@ -703,6 +715,10 @@ namespace Barotrauma
SelectedLocation = location;
SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
if (SelectedConnection?.Locked ?? false)
{
DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
}
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
}
@@ -736,7 +752,7 @@ namespace Barotrauma
public void SelectRandomLocation(bool preferUndiscovered)
{
List<Location> nextLocations = CurrentLocation.Connections.Select(c => c.OtherLocation(CurrentLocation)).ToList();
List<Location> nextLocations = CurrentLocation.Connections.Where(c => !c.Locked).Select(c => c.OtherLocation(CurrentLocation)).ToList();
List<Location> undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered);
if (undiscoveredLocations.Count > 0 && preferUndiscovered)
@@ -943,9 +959,9 @@ namespace Barotrauma
/// <summary>
/// Load a previously saved map from an xml element
/// </summary>
public static Map Load(CampaignMode campaign, XElement element)
public static Map Load(CampaignMode campaign, XElement element, CampaignSettings settings)
{
Map map = new Map(campaign, element);
Map map = new Map(campaign, element, settings);
map.LoadState(element, false);
#if CLIENT
map.DrawOffset = -map.CurrentLocation.MapPosition;
@@ -1079,6 +1095,7 @@ namespace Barotrauma
var connectionElement = new XElement("connection",
new XAttribute("passed", connection.Passed),
new XAttribute("locked", connection.Locked),
new XAttribute("difficulty", connection.Difficulty),
new XAttribute("biome", connection.Biome.Identifier),
new XAttribute("locations", Locations.IndexOf(connection.Locations[0]) + "," + Locations.IndexOf(connection.Locations[1])));
@@ -1,4 +1,5 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -13,6 +14,9 @@ namespace Barotrauma
[Serialize(defaultValue: 0f, isSaveable: true)]
public float Amount { get; set; }
[Serialize(defaultValue: true, isSaveable: true)]
public bool Enabled { get; set; }
public Dictionary<string, SerializableProperty> SerializableProperties { get; }
public readonly Map Map;
@@ -23,8 +27,6 @@ namespace Barotrauma
private float increasedAmount;
private float lastIncrease;
public bool Enabled = true;
public Radiation(Map map, RadiationParams radiationParams, XElement? element = null)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
@@ -52,6 +54,12 @@ namespace Barotrauma
foreach (Location location in Map.Locations.Where(Contains))
{
if (location.IsGateBetweenBiomes)
{
location.Connections.ForEach(c => c.Locked = false);
continue;
}
if (amountOfOutposts <= Params.MinimumOutpostAmount) { break; }
if (Map.CurrentLocation is { } currLocation)
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -29,6 +30,15 @@ namespace Barotrauma
[Serialize(defaultValue: 1f, isSaveable: false, "How much is the radiation affliction increased by while in a radiated zone.")]
public float RadiationDamageAmount { get; set; }
[Serialize(defaultValue: "139,0,0,85", isSaveable: false, "The color of the radiated area.")]
public Color RadiationAreaColor { get; set; }
[Serialize(defaultValue: "255,0,0,255", isSaveable: false, "The tint of the radiation border sprites.")]
public Color RadiationBorderTint { get; set; }
[Serialize(defaultValue: 16.66f, isSaveable: false, "Speed of the border spritesheet animation.")]
public float BorderAnimationSpeed { get; set; }
public RadiationParams(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
@@ -67,6 +67,9 @@ namespace Barotrauma
set;
}
[Serialize("", isSaveable: true), Editable]
public string ReplaceInRadiation { get; set; }
private readonly Dictionary<string, int> moduleCounts = new Dictionary<string, int>();
public IEnumerable<KeyValuePair<string, int>> ModuleCounts
@@ -68,6 +68,15 @@ namespace Barotrauma
private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false)
{
var outpostModuleFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.OutpostModule);
if (location != null)
{
if (location.IsCriticallyRadiated() && OutpostGenerationParams.Params.FirstOrDefault(p => p.Identifier.Equals(generationParams.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newParams)
{
generationParams = newParams;
}
locationType = location.GetLocationType();
}
//load the infos of the outpost module files
List<SubmarineInfo> outpostModules = new List<SubmarineInfo>();
@@ -974,7 +983,7 @@ namespace Barotrauma
var moduleEntities = MapEntity.LoadAll(sub, hallwayInfo.SubmarineElement, hallwayInfo.FilePath, -1);
//remove items that don't fit in the hallway
moduleEntities.Where(e => e is Item item && item.GetComponent<Door>() == null && e.Rect.Width > hallwayLength).ForEach(e => e.Remove());
moduleEntities.Where(e => e is Item item && item.GetComponent<Door>() == null && (isHorizontal ? e.Rect.Width : e.Rect.Height) > hallwayLength).ForEach(e => e.Remove());
//find the largest hull to use it as the center point of the hallway
//and the bounds of all the hulls, used when resizing the hallway to fit between the modules
@@ -1047,11 +1056,11 @@ namespace Barotrauma
}
}
}
else if (me is Structure structure)
else if (me is Structure || (me is Item item && item.GetComponent<Door>() == null))
{
if (isHorizontal)
{
if (!structure.ResizeHorizontal)
if (!me.ResizeHorizontal)
{
int xPos = (int)(leftHull.WorldRect.Right + (me.WorldPosition.X - hullBounds.X) * scaleFactor);
me.Rect = new Rectangle(xPos - me.RectWidth / 2, me.Rect.Y, me.Rect.Width, me.Rect.Height);
@@ -1065,9 +1074,9 @@ namespace Barotrauma
}
else
{
if (!structure.ResizeVertical)
if (!me.ResizeVertical)
{
int yPos = (int)(topHull.WorldRect.Y - topHull.RectHeight + (me.WorldPosition.X - hullBounds.Bottom) * scaleFactor);
int yPos = (int)(topHull.WorldRect.Y - topHull.RectHeight + (me.WorldPosition.Y - hullBounds.Bottom) * scaleFactor);
me.Rect = new Rectangle(me.Rect.X, yPos + me.RectHeight / 2, me.Rect.Width, me.Rect.Height);
}
else
@@ -1394,7 +1403,7 @@ namespace Barotrauma
ISpatialEntity gotoTarget = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, humanPrefab.GetModuleFlags(), humanPrefab.GetSpawnPointTags());
if (gotoTarget == null)
{
gotoTarget = outpost.GetHulls(true).GetRandom();
gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.Server);
}
characterInfo.TeamID = CharacterTeamType.FriendlyNPC;
var npc = Character.Create(CharacterPrefab.HumanConfigFile, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
@@ -590,10 +590,11 @@ namespace Barotrauma
if (item.GetComponent<Turret>() != null) { return false; }
if (item.body != null && !item.body.Enabled) { return true; }
}
if (e.HiddenInGame) { return true; }
return false;
});
if (entities.Count == 0) return Rectangle.Empty;
if (entities.Count == 0) { return Rectangle.Empty; }
float minX = entities[0].Rect.X, minY = entities[0].Rect.Y - entities[0].Rect.Height;
float maxX = entities[0].Rect.Right, maxY = entities[0].Rect.Y;
@@ -1356,7 +1357,7 @@ namespace Barotrauma
if (me.Submarine != this) { continue; }
if (me is Item item)
{
item.SpawnedInOutpost = !info.OutpostGenerationParams.AllowStealing;
item.SpawnedInOutpost = info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AllowStealing;
if (item.GetComponent<Repairable>() != null && indestructible)
{
item.Indestructible = true;
@@ -1366,7 +1367,7 @@ namespace Barotrauma
if (ic is ConnectionPanel connectionPanel)
{
//prevent rewiring
if (!info.OutpostGenerationParams.AlwaysRewireable)
if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable)
{
connectionPanel.Locked = true;
}
@@ -11,7 +11,9 @@ using Barotrauma.Extensions;
namespace Barotrauma
{
public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 3, Corpse = 4 };
[Flags]
public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8 };
partial class WayPoint : MapEntity
{
public static List<WayPoint> WayPointList = new List<WayPoint>();
@@ -13,6 +13,7 @@ namespace Barotrauma.Networking
public string Name; public UInt16 NameID;
public byte ID;
public UInt64 SteamID;
public UInt64 OwnerSteamID;
public string Language;
@@ -21,6 +21,12 @@ namespace Barotrauma.Networking
protected set;
}
public UInt64 OwnerSteamID
{
get;
protected set;
}
public string EndPointString
{
get;
@@ -44,5 +50,15 @@ namespace Barotrauma.Networking
//is received by the server.
return false;
}
public bool SetOwnerSteamIDIfUnknown(UInt64 id)
{
//we know that for both Lidgren and SteamP2P, the
//owner id isn't known until the auth ticket is
//processed, so this method is the same for both
if (OwnerSteamID != 0) { return false; }
OwnerSteamID = id;
return true;
}
}
}
@@ -10,6 +10,7 @@ namespace Barotrauma.Networking
public SteamP2PConnection(string name, UInt64 steamId)
{
SteamID = steamId;
OwnerSteamID = 0;
EndPointString = SteamManager.SteamIDUInt64ToString(SteamID);
Name = name;
Heartbeat();
@@ -872,6 +872,13 @@ namespace Barotrauma.Networking
private set;
}
[Serialize(true, true)]
public bool RadiationEnabled
{
get;
set;
}
public void SetPassword(string password)
{
if (string.IsNullOrEmpty(password))
@@ -30,7 +30,7 @@ namespace Barotrauma
var collision = prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier);
if (collision != null)
{
DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {nameof(T)}: {prefab.Identifier} has the same identifier as {collision.Identifier} ({prefab.UIntIdentifier})");
DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same identifier as {collision.Identifier} ({prefab.UIntIdentifier})");
collision.UIntIdentifier++;
}
}
@@ -93,7 +93,7 @@ namespace Barotrauma
//Handle bad overrides and duplicates
if (basePrefabExists && !isOverride)
{
DebugConsole.ThrowError($"Error registering \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T).ToString()}): base already exists; try overriding");
DebugConsole.ThrowError($"Error registering \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T).ToString()}): base already exists; try overriding\n{Environment.StackTrace}");
return;
}
@@ -36,6 +36,22 @@ namespace Barotrauma
levelDifficultyScrollBar.OnMoved(levelDifficultyScrollBar, levelDifficultyScrollBar.BarScroll);
#endif
}
public void SetRadiationEnabled(bool enabled)
{
#if CLIENT
radiationEnabledTickBox.Selected = enabled;
#endif
}
public bool IsRadiationEnabled()
{
#if CLIENT
return radiationEnabledTickBox.Selected;
#elif SERVER
return GameMain.Server.ServerSettings.RadiationEnabled;
#endif
}
public void ToggleTraitorsEnabled(int dir)
{
@@ -570,6 +570,25 @@ namespace Barotrauma
public static Color ParseColor(string stringColor, bool errorMessages = true)
{
if (stringColor.StartsWith("gui.", StringComparison.OrdinalIgnoreCase))
{
#if CLIENT
if (GUI.Style != null)
{
string colorName = stringColor.Substring(4);
var property = GUI.Style.GetType().GetProperties().FirstOrDefault(
p => p.PropertyType == typeof(Color) &&
p.Name.Equals(colorName, StringComparison.OrdinalIgnoreCase));
if (property != null)
{
return (Color)property?.GetValue(GUI.Style);
}
}
#endif
return Color.White;
}
string[] strComponents = stringColor.Split(',');
Color color = Color.White;
@@ -39,6 +39,66 @@ namespace Barotrauma
}
}
class AITrigger : ISerializableEntity
{
public string Name => "ai trigger";
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
[Serialize(AIState.Idle, false)]
public AIState State { get; private set; }
[Serialize(0f, false)]
public float Duration { get; private set; }
[Serialize(1f, false)]
public float Probability { get; private set; }
[Serialize(0f, false)]
public float MinDamage { get; private set; }
[Serialize(true, false)]
public bool AllowToOverride { get; private set; }
[Serialize(true, false)]
public bool AllowToBeOverridden { get; private set; }
public bool IsTriggered { get; private set; }
public float Timer { get; private set; } = -1;
public bool IsActive { get; private set; }
public void Launch()
{
IsTriggered = true;
IsActive = true;
Timer = Duration;
}
public void Reset()
{
IsTriggered = false;
IsActive = false;
Timer = 0;
}
public void UpdateTimer(float deltaTime)
{
Timer -= deltaTime;
if (Timer < 0)
{
Timer = 0;
IsActive = false;
}
}
public AITrigger(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
}
partial class StatusEffect
{
[Flags]
@@ -203,6 +263,7 @@ namespace Barotrauma
private readonly List<ItemSpawnInfo> spawnItems;
private readonly List<CharacterSpawnInfo> spawnCharacters;
private readonly List<AITrigger> aiTriggers;
private readonly List<EventPrefab> triggeredEvents;
private readonly string triggeredEventTargetTag = "statuseffecttarget",
@@ -282,6 +343,7 @@ namespace Barotrauma
requiredItems = new List<RelatedItem>();
spawnItems = new List<ItemSpawnInfo>();
spawnCharacters = new List<CharacterSpawnInfo>();
aiTriggers = new List<AITrigger>();
Afflictions = new List<Affliction>();
Explosions = new List<Explosion>();
triggeredEvents = new List<EventPrefab>();
@@ -532,7 +594,6 @@ namespace Barotrauma
triggeredEvents.Add(prefab);
}
}
foreach (XElement eventElement in subElement.Elements())
{
if (!eventElement.Name.ToString().Equals("ScriptedEvent", StringComparison.OrdinalIgnoreCase)) { continue; }
@@ -543,6 +604,9 @@ namespace Barotrauma
var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName);
if (!string.IsNullOrWhiteSpace(newSpawnCharacter.SpeciesName)) { spawnCharacters.Add(newSpawnCharacter); }
break;
case "aitrigger":
aiTriggers.Add(new AITrigger(subElement));
break;
}
}
InitProjSpecific(element, parentDebugName);
@@ -1032,7 +1096,7 @@ namespace Barotrauma
{
targetCharacter = character;
}
else if (target is Limb limb)
else if (target is Limb limb && !limb.Removed)
{
targetLimb = limb;
targetCharacter = limb.character;
@@ -1053,6 +1117,32 @@ namespace Barotrauma
#endif
}
}
if (aiTriggers.Any())
{
Character targetCharacter = target as Character;
if (targetCharacter == null)
{
if (target is Limb targetLimb && !targetLimb.Removed)
{
targetCharacter = targetLimb.character;
}
}
if (targetCharacter != null && !targetCharacter.Removed && !targetCharacter.IsPlayer)
{
if (targetCharacter.AIController is EnemyAIController enemyAI)
{
foreach (AITrigger trigger in aiTriggers)
{
if (Rand.Value(Rand.RandSync.Unsynced) > trigger.Probability) { continue; }
if (target is Limb targetLimb && targetCharacter.LastDamage.HitLimb != targetLimb) { continue; }
if (targetCharacter.LastDamage.Damage < trigger.MinDamage) { continue; }
enemyAI.LaunchTrigger(trigger);
break;
}
}
}
}
}
if (FireSize > 0.0f && entity != null)
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using Barotrauma.Extensions;
using System.Linq;
namespace Barotrauma
{
@@ -1066,6 +1067,16 @@ namespace Barotrauma
if (diff == 0) { return v >= max ? 1f : 0f; }
return MathHelper.Clamp((v - min) / diff, 0f, 1f);
}
public static float Min(params float[] vals)
{
return vals.Min();
}
public static float Max(params float[] vals)
{
return vals.Max();
}
}
class CompareCCW : IComparer<Vector2>
@@ -7,7 +7,10 @@ namespace Barotrauma.IO
{
static readonly string[] unwritableDirs = new string[] { "Content", "Data/ContentPackages" };
public static bool DevException;
/// <summary>
/// When set to true, the game is allowed to modify the vanilla content in debug builds. Has no effect in non-debug builds.
/// </summary>
public static bool SkipValidationInDebugBuilds;
public static bool CanWrite(string path)
{
@@ -20,7 +23,7 @@ namespace Barotrauma.IO
if (path.StartsWith(dir, StringComparison.InvariantCultureIgnoreCase))
{
#if DEBUG
return DevException;
return SkipValidationInDebugBuilds;
#else
return false;
#endif
@@ -37,7 +40,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(path))
{
DebugConsole.ThrowError($"Cannot save XML document to \"{path}\": failed validation");
DebugConsole.ThrowError($"Cannot save XML document to \"{path}\": modifying the files in the folder is not allowed.");
return;
}
doc.Save(path);
@@ -47,7 +50,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(path))
{
DebugConsole.ThrowError($"Cannot save XML element to \"{path}\": failed validation");
DebugConsole.ThrowError($"Cannot save XML element to \"{path}\": modifying the files in the folder is not allowed.");
return;
}
element.Save(path);
@@ -72,7 +75,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(path))
{
DebugConsole.ThrowError($"Cannot write XML document to \"{path}\": failed validation");
DebugConsole.ThrowError($"Cannot write XML document to \"{path}\": modifying the files in the folder is not allowed.");
Writer = null;
return;
}
@@ -222,7 +225,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(path))
{
DebugConsole.ThrowError($"Cannot create directory \"{path}\": failed validation");
DebugConsole.ThrowError($"Cannot create directory \"{path}\": modifying the contents of the folder is not allowed.");
return null;
}
return System.IO.Directory.CreateDirectory(path);
@@ -232,7 +235,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(path))
{
DebugConsole.ThrowError($"Cannot delete directory \"{path}\": failed validation");
DebugConsole.ThrowError($"Cannot delete directory \"{path}\": modifying the contents of the folder is not allowed.");
return;
}
//TODO: validate recursion?
@@ -251,7 +254,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(dest))
{
DebugConsole.ThrowError($"Cannot copy \"{src}\" to \"{dest}\": failed validation");
DebugConsole.ThrowError($"Cannot copy \"{src}\" to \"{dest}\": modifying the contents of the folder is not allowed.");
return;
}
System.IO.File.Copy(src, dest, overwrite);
@@ -261,12 +264,12 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(src))
{
DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": src failed validation");
DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": modifying the contents of the source folder is not allowed.");
return;
}
if (!Validation.CanWrite(dest))
{
DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": dest failed validation");
DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": modifying the contents of the destination folder is not allowed");
return;
}
System.IO.File.Move(src, dest);
@@ -276,7 +279,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(path))
{
DebugConsole.ThrowError($"Cannot delete file \"{path}\": failed validation");
DebugConsole.ThrowError($"Cannot delete file \"{path}\": modifying the contents of the folder is not allowed.");
return;
}
System.IO.File.Delete(path);
@@ -298,7 +301,7 @@ namespace Barotrauma.IO
case System.IO.FileMode.Truncate:
if (!Validation.CanWrite(path))
{
DebugConsole.ThrowError($"Cannot open \"{path}\" in {mode} mode: failed validation");
DebugConsole.ThrowError($"Cannot open \"{path}\" in {mode} mode: modifying the contents of the folder is not allowed.");
return null;
}
break;
@@ -328,7 +331,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(path))
{
DebugConsole.ThrowError($"Cannot write all bytes to \"{path}\": failed validation");
DebugConsole.ThrowError($"Cannot write all bytes to \"{path}\": modifying the files in the folder is not allowed.");
return;
}
System.IO.File.WriteAllBytes(path, contents);
@@ -338,7 +341,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(path))
{
DebugConsole.ThrowError($"Cannot write all text to \"{path}\": failed validation");
DebugConsole.ThrowError($"Cannot write all text to \"{path}\": modifying the files in the folder is not allowed.");
return;
}
System.IO.File.WriteAllText(path, contents, encoding ?? System.Text.Encoding.UTF8);
@@ -348,7 +351,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(path))
{
DebugConsole.ThrowError($"Cannot write all lines to \"{path}\": failed validation");
DebugConsole.ThrowError($"Cannot write all lines to \"{path}\": modifying the files in the folder is not allowed.");
return;
}
System.IO.File.WriteAllLines(path, contents, encoding ?? System.Text.Encoding.UTF8);
@@ -420,7 +423,7 @@ namespace Barotrauma.IO
}
else
{
DebugConsole.ThrowError($"Cannot write to file \"{fileName}\": failed validation");
DebugConsole.ThrowError($"Cannot write to file \"{fileName}\": modifying the files in the folder is not allowed.");
}
}
@@ -487,7 +490,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(innerInfo.FullName))
{
DebugConsole.ThrowError($"Cannot delete directory \"{Name}\": failed validation");
DebugConsole.ThrowError($"Cannot delete directory \"{Name}\": modifying the contents of the folder is not allowed.");
return;
}
innerInfo.Delete();
@@ -523,7 +526,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(innerInfo.FullName))
{
DebugConsole.ThrowError($"Cannot set read-only to {value} for \"{Name}\": failed validation");
DebugConsole.ThrowError($"Cannot set read-only to {value} for \"{Name}\": modifying the files in the folder is not allowed.");
return;
}
innerInfo.IsReadOnly = value;
@@ -534,7 +537,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(dest))
{
DebugConsole.ThrowError($"Cannot copy \"{Name}\" to \"{dest}\": failed validation");
DebugConsole.ThrowError($"Cannot copy \"{Name}\" to \"{dest}\": modifying the contents of the destination folder is not allowed.");
return;
}
innerInfo.CopyTo(dest, overwriteExisting);
@@ -544,7 +547,7 @@ namespace Barotrauma.IO
{
if (!Validation.CanWrite(innerInfo.FullName))
{
DebugConsole.ThrowError($"Cannot delete file \"{Name}\": failed validation");
DebugConsole.ThrowError($"Cannot delete file \"{Name}\": modifying the files in the folder is not allowed.");
return;
}
innerInfo.Delete();
@@ -224,6 +224,38 @@ namespace Barotrauma
return inputType;
}
/// <summary>
/// Convert a HSV value into a RGB value.
/// </summary>
/// <param name="hue">Value between 0 and 360</param>
/// <param name="saturation">Value between 0 and 1</param>
/// <param name="value">Value between 0 and 1</param>
/// <see href="https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB">Reference</see>
/// <returns></returns>
public static Color HSVToRGB(float hue, float saturation, float value)
{
float c = value * saturation;
float h = Math.Clamp(hue, 0, 360) / 60f;
float x = c * (1 - Math.Abs(h % 2 - 1));
float r = 0,
g = 0,
b = 0;
if (0 <= h && h <= 1) { r = c; g = x; b = 0; }
else if (1 < h && h <= 2) { r = x; g = c; b = 0; }
else if (2 < h && h <= 3) { r = 0; g = c; b = x; }
else if (3 < h && h <= 4) { r = 0; g = x; b = c; }
else if (4 < h && h <= 5) { r = x; g = 0; b = c; }
else if (5 < h && h <= 6) { r = c; g = 0; b = x; }
float m = value - c;
return new Color(r + m, g + m, b + m);
}
/// <summary>
/// Returns either a green [x] or a red [o]
/// </summary>
@@ -491,6 +523,30 @@ namespace Barotrauma
return destination;
}
public static void SiftElement<T>(this List<T> list, int from, int to)
{
if (from < 0 || from >= list.Count) { throw new ArgumentException($"from parameter out of range (from={from}, range=[0..{list.Count - 1}])"); }
if (to < 0 || to >= list.Count) { throw new ArgumentException($"to parameter out of range (to={to}, range=[0..{list.Count - 1}])"); }
T elem = list[from];
if (from > to)
{
for (int i = from; i > to; i--)
{
list[i] = list[i - 1];
}
list[to] = elem;
}
else if (from < to)
{
for (int i = from; i < to; i++)
{
list[i] = list[i + 1];
}
list[to] = elem;
}
}
public static string ByteArrayToString(byte[] ba)
{
StringBuilder hex = new StringBuilder(ba.Length * 2);