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
@@ -62,6 +62,7 @@
<Character file="Content/Characters/Balloon/Balloon.xml" />
<Character file="Content/Characters/Carrier/Carrier.xml" />
<Character file="Content/Characters/Charybdis/Charybdis.xml" />
<Character file="Content/Characters/Charybdisold/Charybdisold.xml" />
<Character file="Content/Characters/Coelanth/Coelanth.xml" />
<Character file="Content/Characters/Crawler/Crawler.xml" />
<Character file="Content/Characters/Crawlerhusk/Crawlerhusk.xml" />
@@ -248,4 +249,6 @@
<OutpostModule file="Content/Map/Outposts/Hall4wayModule_01_Abandoned.sub" />
<BeaconStation file="Content/Map/BeaconStations/BeaconStation1.sub" />
<OutpostModule file="Content/Map/Outposts/MineModule_04.sub" />
<OutpostModule file="Content/Map/Outposts/HallModuleHorizontal_Abandoned.sub" />
<OutpostModule file="Content/Map/Outposts/HallModuleVertical_Abandoned.sub" />
</contentpackage>
@@ -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);
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+115 -4
View File
@@ -1,5 +1,86 @@
---------------------------------------------------------------------------------------------------------
v0.12.X.X
v0.1300.0.1 (unstable)
---------------------------------------------------------------------------------------------------------
Changes:
- 3 new outpost mission types: clearing a nest, hostage rescue and assassination.
- The enemies in abandoned outposts are tougher and there's more of them in multiplayer.
- Improvements to abandoned outpost modules.
- Respawning is disabled in abandoned outposts.
- Gating progress between biomes: you need a certain amount of money or reputation before you can enter the next biome.
- Reworked Charybdis as another abyss monster (in addition to Endworm).
- Revisited husk animations, movement speeds, and general balance. Husks regenerate a lot and are a bit tougher than they use to be. Human husks can and will easily rise again, unless properly killed.
- Adjustments and fixes on the thresher attacks.
- Revisit mudraptors' attacks. Fixes unarmored mudraptors and mudraptor hatchlings acting weird when attacking characters in water.
- Lower the overall damage of all the hatchlings.
- Bots can now be renamed through the outpost crew management interface.
- Fabricators now display remaining time when fabricating.
- Endworm now groans when the tail is cut.
- Beacon stations' reactors don't deteriorate or consuming fuel to prevent the station from going back down when the player is travelling out from the level.
- Irradiated outposts become abandoned instead of simply disappearing.
- Fabricator displays the time until ready when fabrication is in progress.
- The output length of all signal components is now restricted to 200 characters by default to prevent performance and networking issues if the output is set to an excessively long value (such as the entire dialog of the Bee Movie). The limit can be increased in the sub editor.
- Stunning still works on friendly characters even if friendly fire has been disabled on a server.
- Made radiation sickness's icon appear before it starts causing burns (instead of working the other way around).
- Made radiation sickness cause nausea.
- Added a checkbox to color component that toggles between RGBA and HSV input.
- Disabled NPC conversations in sub editor test mode.
- Added a tooltip when hovering over radiated area.
- Added a checkbox for the radiation feature to both singleplayer and multiplayer.
- Radiated outposts now turn into abandoned outposts instead of vanishing in thin air.
- Use ternary states for some of the server filters (thanks someone972!)
- Banning a player who's using Steam Family Sharing will now also ban the owner's account, solving ban evasion using this method.
- Added interactive submarine previews to the lobby and "New Game" screen.
- Implemented rudimentary text highlighting in mission descriptions.
- Added in-game hints, designed to help with new player onboarding. Can be disabled in the settings.
Fixes:
- Attempt to fix crashes with the error message "failed to generate OpenAL/stream buffer" on Mac.
- Waypoint fixes in the abandoned outposts.
- Fixed bot reaction times and readjusted aiming delays.
- Fixed bots that follow you not always holding position in the combat mode.
- Fixed bots not being able to hit targets that lie on the ground.
- Fixed monsters sometimes targeting ruin rooms.
- Monsters now stop targeting characters that are far enough behind level geometry. Fixes them getting stuck while trying to reach targets that are not easily reachable.
- Fixes to the Endworm's behavior and ragdoll.
- Minor boost to the Endworm's attack on walls, major boost to the attack on characters.
- Fixed a crash when no valid limb was found for an affliction. Probably only happened with characters that already had some limbs severed.
- Fixed non-multiplied damage to limbs effectively always being clamped to 100, which means that no attack could do more than 100 damage per hit unless there's some damage modifier defined on the target limb.
- Fixed husk's mouth tentacles rendering on top of the left hand. Also fixed a minor texture bleeding on the waist.
- Fixed possible desync when multiple players use the crew management interface simultaneously.
- Fixed being able to drag order icons when spectating.
- Fixed non-interactable items being counted as owned in the store interface.
- Fixed abyss monsters not keeping in the depths and non-abyss monsters not avoiding the depths when they have attack targets. Now they should only attack the targets that come to their zone and continue pursuing the target until they either lose it or if the state changes from attack to something else.
- Fixed bots treating radiation sickness too eagerly (leading to wasted antirad).
- Added missing platforms to abandoned outpost modules.
- Fixed vanilla wrecks/modules/outposts being shown in the Workshop menu's publish tab.
- Fixed bioluminescent cave's hallucination effect never going fully away and made it treatable with haloperidol.
- Fixed characters getting impact damage from collisions with sensors. Caused characters to get stunned when they're thrown around by a monster while touching a level trigger (for instance, the branches in forest caves).
- Fixed held items not appearing in the characters hand after entering a new level in the campaign.
- Fixed hidden items being shown when searching for them in sub editor.
- Fixed crashing when trying to load a linked sub that contains no hulls.
- Fixed "hide incompatible" checkbox in the server list.
- Fixed one of the engineer variants having too many items in the toolbelt.
- Monster events don't spawn an additional endworm in the abyss if one has already been spawned by a hunting grounds mission.
- Fixed nav terminal ignoring velocity_in signals when the terminal hasn't been operated by anyone during the round.
- Fixed launching depth charges crashing the game.
- Fixed coilgun lights flickering when firing.
- Fixed ability to walk through the heavy doors in mining outposts.
- Fixed inability to move stacks of items to containers by double-clicking.
- Fixed crashing when trying to enter an abandoned outpost with a sub that has shuttles.
- Fixed monsters targeting entities outside of their allowed zone. They dropped the target when they reached the non-allowed zone, which caused indetermined behavior (fluctuating) at the border.
- Fixed monsters with the circling behavior sometimes not being able to reach the target because they were not targeting the closest target.
- Fixed Workshop mod download prompt looping and freezing when there's a hash mismatch after a mod has already been installed.
Modding:
- Added AITrigger that can be used to trigger an ai state using the status effects (See Charybdis).
- Turned IsTraitor into a property so it can be accessed by status effects.
Bots:
- Fixed non-security bots fleeing the enemy (in defensive combat state) even when they are ordered to fight intruders.
---------------------------------------------------------------------------------------------------------
v0.1300.0.0 (unstable)
---------------------------------------------------------------------------------------------------------
Campaign changes:
@@ -12,17 +93,21 @@ Campaign changes:
Abyss:
- Reintroduced Endworms.
- Added floating islands that contain caves and rare minerals to the Abyss.
- Multiple current orders: you can assign up to three simultaneous orders for characters and drag the icons to change their priority.
Changes:
- Added abandoned outposts and a new abandoned outpost mission type.
- Added entity subcategories to the submarine editor (note that most of the vanilla items/structures aren't categorized yet in this build).
- Reworked the impacts to the sub when it's hit by monsters. Increased the screen shake and stunning. There's now a 30 second cooldown for getting knocked down. Fixed the impact not triggering if the colliding limb is not big enough.
- Reworked attacks and effects for the following creatures: Moloch, Black Moloch, Hammerhead, Hammerhead Matriarch, and Golden Hammerhead. They now have bigger impact on the sub when they hit it. Black Moloch's emp damage is halved.
- Moloch's shell now always breaks when shot with a railgun.
- Recreated waypoints for the vanilla subs.
- Added EMP effect to nuclear shells.
- Added EMP effect to nuclear shells. Increased the damage a bit.
- Added a right click context menu option to copy debug console errors to clipboard.
- Railgun shells now explode instead of piercing armor. Physicorium shells still go through armor.
- Railgun shells now explode instead of piercing armor. Physicorium shells still go through armor. Make all railgun ammunition slightly more likely to break limb joints.
- Added multiple current orders: you can assign up to three simultaneous orders for characters and drag the icons to change their priority.
- NPCs (non-bots) speak report related lines less than they used to.
- The bleeding particles are now emitted from the last matching limb instead of the first. Readjusted particle emit frequency and scale.
- Reduced Moloch's bleeding reductions.
Fixes:
- Major improvements to the voice chat: higher audio quality, less intrusive radio effect, fixed clicks/distortion.
@@ -43,6 +128,10 @@ Fixes:
- Fixed status monitor in a docked shuttle sometimes not displaying the main sub.
- Fixed campaign stores sometimes displaying prices for a previous location.
- Fixed various scaling issues on higher resolutions.
- Fixed monsters fluttering while pursuing other creatures. Only happened while swimming and was not notable on all monsters).
- Fixed mouse cursor being switched to hand even when the spritesheet is not shown in the character editor.
- Fixed monsters with low head/torso torques moving backwards when they try to turn around 180 degrees.
- Fixed regular Moloch not bleeding correctly.
Bots:
- Bots now warn when you are running low / out of oxygen or welding fuel tanks, turret ammunition, or reactor fuel.
@@ -58,11 +147,33 @@ Bots:
- Fixed bots trying to return the diving suit when it's not a reasonable thing to do, but when they actually don't need it.
- Fixed bots failing to swap an oxygen tank from a diving mask to a diving suit when both items are equipped.
- Fixed bots getting confused when they have a mask without a valid oxygen tank inside it and when there's not enough oxygen in the room to be without the mask.
- Bots now react faster in general to all enemies.
- Bots now automatically attack enemies outside of the main sub, if they have a weapon. While docked to an enemy outpost or submarine, the "fight intruders" order now acts as an offensive order to attack the enemies on the connected submarine/outpost.
- Fixed bots following the player when the player is controlling a monster.
- You can now escape from NPCs by going far enough from them while they are pursuing you.
- Fixed bots reacting to attackers that are outside of the submarine.
- Fixed the "reportrange" parameter not working when the character is attacked. In practice only has effect on NPCs.
- Fixed report icons being shown also for other teams instead of just the own team.
- Bots now target the closest limb instead of the main collider when they aim with the turret. With small creatures, this should now make any difference. With long creatures, it allows the bots to target the extremities of the body instead of always shooting at the main body.
Modding:
- The hit impact of a monster's attack can now be adjusted with the new "submarineimpactmultiplier" defined in the attack block. Note that this is a multipler to the actual impact, hence also the force applied on the attacking monster affects the final impact.
- Explosions now have three new parameters: ignorecover, onlyinside, and onlyoutside.
- Fixed crashing when attempting to play a music clip that isn't a valid ogg file or if the file is not found.
- Fixed crashing when trying to spawn a character variant with custom inventory contents.
- Renamed the ai parameter "Threshold" as "DamageThreshold".
- Replaced "spawndeep" with "abyss" spawn type (defined in random events).
- New parameters StayInAbyss and StayInsideLevel that define the area where the creature tries to keep inside while not attacking.
- The creature disable distance, which totally disables the creature, is now exposed in the character parameters. The distance for triggering simple physics, which disables all the limbs and keeps only the main collider updated, is half of this distance. Increased the default from 22 000 to 25 000 (pixels).
- New attack pattern: Circle (around the target). Used by the abyss creatures.
- The aim speed and accuracy of NPC characters can now be adjusted in the npc (spawn) definition. The Aim speed also affects melee attack speed.
- Fixed OnSevered status effects launching also on the limb that the severed limb was attached to.
- Added "bleedingnonstop" affliction, which is just the same as normal bleeding but it never wears off.
- Added and option to target the last matching limb insted of the first (StatusEffect.TargetType.Limb). Implemented targeting other limbs even when the status effect is triggered from the limb, which was previously only implemented for status effects that targeted character. See Endworm for an example.
- Fixed hidden limbs not being ignored in many cases where they should, which potentially could cause issues with some custom monsters.
- Added "bleedparticlemultiplier" parameter in character definition, which can be used to increase/decrease the general amount of bleeding for the character in question.
- Added an option to always ignore an ai target if it's not inside the same sub as the character.
- Attacks can now "blink" limbs when they attack (Endworm). Blinking is a generic way to rotate limbs so that they "animate" (see Watcher's eye).
---------------------------------------------------------------------------------------------------------
v0.12.0.3