Unstable 0.15.15.0 (and the one before it I forgor)

This commit is contained in:
Markus Isberg
2021-11-18 21:34:30 +09:00
parent 10e5fd5f3e
commit 80f39cd2a3
257 changed files with 4916 additions and 2582 deletions
@@ -64,7 +64,7 @@ namespace Barotrauma
#endif
}
private IEnumerable<object> Update(ISpatialEntity targetEntity, Camera cam)
private IEnumerable<CoroutineStatus> Update(ISpatialEntity targetEntity, Camera cam)
{
if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; }
@@ -229,7 +229,7 @@ namespace Barotrauma
{
if (sectorRad >= MathHelper.TwoPi) { return true; }
Vector2 diff = worldPosition - WorldPosition;
return MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir)) <= sectorRad * 0.5f;
return Math.Abs(MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir))) <= sectorRad * 0.5f;
}
public void Remove()
@@ -524,11 +524,12 @@ namespace Barotrauma
if (Character.LockHands) { return; }
if (ObjectiveManager.CurrentObjective == null) { return; }
if (Character.CurrentHull == null) { return; }
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold && Character.NeedsOxygen;
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
{
if (!Character.NeedsAir) { return false; }
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
Hull targetHull = gotoObjective.GetTargetHull();
return gotoObjective.Target != null && targetHull == null ||
@@ -567,6 +568,7 @@ namespace Barotrauma
Character.AnimController.HeadInWater ||
Character.Submarine == null ||
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOn) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
@@ -625,7 +627,7 @@ namespace Barotrauma
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
if (divingSuit != null)
if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS))
{
if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
@@ -950,7 +952,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.RepairIconThreshold)) { continue; }
if (!item.Repairables.Any(r => r.IsBelowRepairIconThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
@@ -1117,6 +1119,7 @@ namespace Barotrauma
// Don't react to attackers that are outside of the sub (e.g. AoE attacks)
return;
}
bool isAttackerFightingEnemy = false;
if (IsFriendly(attacker))
{
if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
@@ -1136,7 +1139,6 @@ namespace Barotrauma
}
else
{
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
// Inform other NPCs
if (cumulativeDamage > 1 || totalDamage >= 10)
{
@@ -1184,6 +1186,10 @@ namespace Barotrauma
}
}
}
if (!isAttackerFightingEnemy)
{
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
}
}
}
else
@@ -1211,7 +1217,15 @@ namespace Barotrauma
if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; }
if (!otherHumanAI.IsFriendly(Character)) { continue; }
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
if (!isWitnessing && !CheckReportRange(Character, otherCharacter, ReportRange)) { continue; }
if (!isWitnessing)
{
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
//don't react to the attack
if (Character.IsDead || Character.IsUnconscious || !CheckReportRange(Character, otherCharacter, ReportRange))
{
continue;
}
}
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing, dmgThreshold: attacker.TeamID == Character.TeamID ? 50 : 10);
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
@@ -1244,18 +1258,20 @@ namespace Barotrauma
return AIObjectiveCombat.CombatMode.None;
}
// If there are any enemies around, just ignore the friendly fire
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsDead && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
{
isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None;
}
else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
{
return Character.CombatAction.WitnessReaction;
}
else if (Character.IsInstigator && attacker.IsPlayer)
else if (attacker.IsPlayer && FindInstigator() is Character instigator)
{
// The guards don't react when the player attacks instigators.
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
// The guards don't react when the player there's an instigator around
isAttackerFightingEnemy = true;
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
}
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
{
@@ -1295,6 +1311,22 @@ namespace Barotrauma
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
}
Character FindInstigator()
{
if (Character.IsInstigator)
{
return Character;
}
else if (c.AIController is HumanAIController humanAi)
{
return Character.CharacterList.FirstOrDefault(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && ch.IsInstigator && humanAi.VisibleHulls.Contains(ch.CurrentHull));
}
else
{
return null;
}
}
}
}
}
@@ -1416,15 +1448,20 @@ namespace Barotrauma
return true;
}
public static bool NeedsDivingGear(Hull hull, out bool needsSuit)
public bool NeedsDivingGear(Hull hull, out bool needsSuit)
{
if (!Character.NeedsAir)
{
needsSuit = false;
return false;
}
needsSuit = false;
if (hull == null ||
hull.WaterPercentage > 90 ||
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f))
{
needsSuit = true;
needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
return true;
}
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
@@ -1570,9 +1607,9 @@ namespace Barotrauma
Character thief = character;
bool someoneSpoke = false;
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
if ((item.SpawnedInOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
{
foreach (Character otherCharacter in Character.CharacterList)
{
@@ -1624,7 +1661,7 @@ namespace Barotrauma
}
}
}
else if (item.OwnInventory?.FindItem(it => it.SpawnedInOutpost && !item.AllowStealing, true) is { } foundItem)
else if (item.OwnInventory?.FindItem(it => it.SpawnedInCurrentOutpost && !item.AllowStealing, true) is { } foundItem)
{
ItemTaken(foundItem, character);
}
@@ -1698,7 +1735,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, character))
{
if (item.Repairables.All(r => item.ConditionPercentage >= r.RepairThreshold)) { continue; }
if (item.Repairables.All(r => r.IsBelowRepairThreshold)) { continue; }
AddTargets<AIObjectiveRepairItems, Item>(character, item);
}
}
@@ -1786,7 +1823,7 @@ namespace Barotrauma
{
if (isCurrentHull)
{
CurrentHullSafety = 0;
CurrentHullSafety = character.NeedsAir ? 0 : 100;
}
return CurrentHullSafety;
}
@@ -1809,8 +1846,8 @@ namespace Barotrauma
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null) { return 0; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; }
if (hull == null) { return character.NeedsAir ? 0 : 100; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0 && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { return 0; }
// Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
// With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
@@ -204,11 +204,7 @@ namespace Barotrauma
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0;
if (newPath.Unreachable || newPath.Nodes.None())
{
useNewPath = false;
}
else if (!useNewPath && currentPath != null && currentPath.CurrentNode != null)
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
if (IsIdenticalPath())
@@ -12,20 +12,19 @@ namespace Barotrauma
class LatchOntoAI
{
const float RaycastInterval = 5.0f;
private float raycastTimer;
private Structure targetWall;
private Body targetBody;
private Vector2 attachSurfaceNormal;
private Submarine targetSubmarine;
private Character targetCharacter;
private readonly Character character;
public bool AttachToSub { get; private set; }
public bool AttachToWalls { get; private set; }
public bool AttachToCharacters { get; private set; }
public Submarine TargetSubmarine { get; private set; }
public Structure TargetWall { get; private set; }
public Character TargetCharacter { get; private set; }
private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration, coolDown;
private readonly float damageOnDetach, detachStun;
private readonly bool weld;
@@ -51,7 +50,7 @@ namespace Barotrauma
public bool IsAttached => AttachJoints.Count > 0;
public bool IsAttachedToSub => IsAttached && targetSubmarine != null && targetCharacter == null;
public bool IsAttachedToSub => IsAttached && TargetSubmarine != null && TargetCharacter == null;
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
{
@@ -93,9 +92,9 @@ namespace Barotrauma
var sub = wall.Submarine;
if (sub == null) { return; }
Reset();
targetWall = wall;
targetSubmarine = sub;
targetBody = targetSubmarine.PhysicsBody.FarseerBody;
TargetWall = wall;
TargetSubmarine = sub;
targetBody = TargetSubmarine.PhysicsBody.FarseerBody;
this.attachSurfaceNormal = attachSurfaceNormal;
_attachPos = attachPos;
}
@@ -103,23 +102,20 @@ namespace Barotrauma
public void SetAttachTarget(Character target)
{
if (!AttachToCharacters) { return; }
if (target.Submarine != character.Submarine) { return; }
Reset();
targetCharacter = target;
targetSubmarine = target.Submarine;
TargetCharacter = target;
targetBody = target.AnimController.Collider.FarseerBody;
attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition);
}
public void Update(EnemyAIController enemyAI, float deltaTime)
{
if (character.Submarine != null)
if (TargetCharacter != null && character.Submarine != TargetCharacter.Submarine ||
character.Submarine != null && TargetSubmarine != null && TargetCharacter == null)
{
if (targetCharacter != null && targetCharacter.Submarine != targetSubmarine ||
character.Submarine != null && targetSubmarine != null && targetCharacter == null)
{
DeattachFromBody(reset: true);
return;
}
DeattachFromBody(reset: true);
return;
}
if (IsAttached)
{
@@ -150,7 +146,7 @@ namespace Barotrauma
return;
}
}
if (targetCharacter != null)
if (TargetCharacter != null)
{
if (enemyAI.AttackingLimb?.attack == null)
{
@@ -159,10 +155,14 @@ namespace Barotrauma
else
{
float range = enemyAI.AttackingLimb.attack.DamageRange * 2f;
if (Vector2.DistanceSquared(targetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range)
if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range)
{
DeattachFromBody(reset: true, cooldown: 1);
}
else
{
TargetCharacter.Latchers.Add(this);
}
}
}
}
@@ -176,15 +176,15 @@ namespace Barotrauma
deattachCheckTimer -= deltaTime;
}
if (targetCharacter != null)
if (TargetCharacter != null)
{
// Own sim pos -> target where we are
_attachPos = character.SimPosition;
}
Vector2 transformedAttachPos = _attachPos;
if (character.Submarine == null && targetSubmarine != null)
if (character.Submarine == null && TargetSubmarine != null)
{
transformedAttachPos += ConvertUnits.ToSimUnits(targetSubmarine.Position);
transformedAttachPos += ConvertUnits.ToSimUnits(TargetSubmarine.Position);
}
if (transformedAttachPos != Vector2.Zero)
{
@@ -267,7 +267,7 @@ namespace Barotrauma
if (enemyAI.AttackingLimb == null) { break; }
if (targetBody == null) { break; }
if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; }
Vector2 referencePos = targetCharacter != null ? targetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos);
Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos);
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
AttachToBody(transformedAttachPos);
@@ -286,11 +286,11 @@ namespace Barotrauma
deattach = true;
attachCooldown = coolDown;
}
if (!deattach && targetWall != null && targetSubmarine != null)
if (!deattach && TargetWall != null && TargetSubmarine != null)
{
// Deattach if the wall is broken enough where we are attached to
int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true);
if (enemyAI.CanPassThroughHole(targetWall, targetSection))
int targetSection = TargetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true);
if (enemyAI.CanPassThroughHole(TargetWall, targetSection))
{
deattach = true;
attachCooldown = coolDown;
@@ -298,7 +298,7 @@ namespace Barotrauma
if (!deattach)
{
// Deattach if the velocity is high
float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length();
float velocity = TargetSubmarine.Velocity == Vector2.Zero ? 0.0f : TargetSubmarine.Velocity.Length();
deattach = velocity > maxDeattachSpeed;
if (!deattach)
{
@@ -385,11 +385,8 @@ namespace Barotrauma
} as Joint;
GameMain.World.Add(colliderJoint);
AttachJoints.Add(colliderJoint);
if (targetCharacter != null)
{
targetCharacter.Latchers.Add(this);
}
AttachJoints.Add(colliderJoint);
TargetCharacter?.Latchers.Add(this);
if (maxAttachDuration > 0)
{
deattachCheckTimer = maxAttachDuration;
@@ -407,25 +404,19 @@ namespace Barotrauma
{
attachCooldown = cooldown;
}
TargetCharacter?.Latchers.Remove(this);
if (reset)
{
Reset();
}
if (targetCharacter != null)
{
targetCharacter.Latchers.Remove(this);
}
}
private void Reset()
{
if (targetCharacter != null)
{
targetCharacter.Latchers.Remove(this);
}
targetCharacter = null;
targetWall = null;
targetSubmarine = null;
TargetCharacter?.Latchers.Remove(this);
TargetCharacter = null;
TargetWall = null;
TargetSubmarine = null;
targetBody = null;
AttachPos = null;
}
@@ -93,13 +93,6 @@ namespace Barotrauma
_abandon = value;
if (_abandon)
{
#if DEBUG
if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && !objectiveManager.IsCurrentOrder<AIObjectiveReturn>())
{
// TODO: dismiss
throw new Exception("Order abandoned!");
}
#endif
OnAbandon();
}
}
@@ -247,7 +240,7 @@ namespace Barotrauma
}
}
protected bool IsAllowed
public bool IsAllowed
{
get
{
@@ -271,7 +264,7 @@ namespace Barotrauma
if (!IsAllowed)
{
Priority = 0;
Abandon = !isOrder;
Abandon = true;
return Priority;
}
if (isOrder)
@@ -94,7 +94,7 @@ namespace Barotrauma
if (item == null) { return false; }
if (item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.SpawnedInOutpost) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
if (item.Container == null)
@@ -294,10 +294,6 @@ namespace Barotrauma
}
}
private bool IsLoaded(ItemComponent weapon, bool checkContainedItems = true) =>
weapon.HasRequiredContainedItems(character, addMessage: false) &&
(!checkContainedItems || weapon.Item.OwnInventory == null || weapon.Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
private bool TryArm()
{
if (character.LockHands || Enemy == null)
@@ -325,7 +321,7 @@ namespace Barotrauma
Weapon = null;
continue;
}
if (IsLoaded(WeaponComponent, checkContainedItems: true))
if (WeaponComponent.IsLoaded(character))
{
// All good, the weapon is loaded
break;
@@ -380,6 +376,7 @@ namespace Barotrauma
constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false)
{
AllowStealing = HumanAIController.IsMentallyUnstable,
EvaluateCombatPriority = false, // Use a custom formula instead
GetItemPriority = i =>
{
if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
@@ -433,7 +430,7 @@ namespace Barotrauma
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!IsLoaded(WeaponComponent))
if (!WeaponComponent.IsLoaded(character))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
@@ -475,7 +472,7 @@ namespace Barotrauma
foreach (var weapon in weaponList)
{
float priority = weapon.CombatPriority;
if (!IsLoaded(weapon))
if (!weapon.IsLoaded(character))
{
if (weapon is RangedWeapon && enemyIsClose)
{
@@ -564,31 +561,6 @@ namespace Barotrauma
}
return weaponComponent.Item;
static Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
{
attack = meleeWeapon.Attack;
}
else if (weapon is RangedWeapon rangedWeapon)
{
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
}
return attack;
}
static float GetLethalDamage(ItemComponent weapon)
{
float lethalDmg = 0;
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
}
return lethalDmg;
}
float ApproximateStunDamage(ItemComponent weapon, Attack attack)
{
// Try to reduce the priority using the actual damage values and status effects.
@@ -628,6 +600,31 @@ namespace Barotrauma
}
}
public static float GetLethalDamage(ItemComponent weapon)
{
float lethalDmg = 0;
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
}
return lethalDmg;
}
private static Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
{
attack = meleeWeapon.Attack;
}
else if (weapon is RangedWeapon rangedWeapon)
{
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
}
return attack;
}
private HashSet<ItemComponent> FindWeaponsFromInventory()
{
weapons.Clear();
@@ -788,7 +785,6 @@ namespace Barotrauma
{
UsePathingOutside = false,
IgnoreIfTargetDead = true,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false
},
@@ -812,7 +808,7 @@ namespace Barotrauma
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs");
if (prefab != null)
{
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInOutpost = true);
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true);
}
}
RemoveFollowTarget();
@@ -144,7 +144,6 @@ namespace Barotrauma
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
@@ -19,10 +19,15 @@ namespace Barotrauma
private AIObjectiveContainItem getOxygen;
private Item targetItem;
public static float MIN_OXYGEN = 10;
public static string HEAVY_DIVING_GEAR = "deepdiving";
public static string LIGHT_DIVING_GEAR = "lightdiving";
public static string OXYGEN_SOURCE = "oxygensource";
public const float MIN_OXYGEN = 10;
public const string HEAVY_DIVING_GEAR = "deepdiving";
public const string LIGHT_DIVING_GEAR = "lightdiving";
/// <summary>
/// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear)
/// </summary>
public const string DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors";
public const string OXYGEN_SOURCE = "oxygensource";
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
@@ -46,10 +46,17 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() ||
objectiveManager.IsCurrentOrder<AIObjectiveReturn>() ||
objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat))
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
if (!character.NeedsAir)
{
Priority = 0;
}
else
{
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() ||
objectiveManager.IsCurrentOrder<AIObjectiveReturn>() ||
objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat))
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
}
else
{
@@ -1,6 +1,7 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Linq;
@@ -11,6 +12,7 @@ namespace Barotrauma
public override string Identifier { get; set; } = "get item";
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowMultipleInstances => true;
public HashSet<Item> ignoredItems = new HashSet<Item>();
@@ -19,7 +21,7 @@ namespace Barotrauma
public float TargetCondition { get; set; } = 1;
public bool AllowDangerousPressure { get; set; }
private readonly string[] identifiersOrTags;
private readonly ImmutableArray<string> identifiersOrTags;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
private bool spawnItemIfNotFound = false;
@@ -31,6 +33,7 @@ namespace Barotrauma
public Item TargetItem => targetItem;
private int currSearchIndex;
public string[] ignoredContainerIdentifiers;
public string[] ignoredIdentifiersOrTags;
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
private readonly bool checkInventory;
@@ -51,6 +54,10 @@ namespace Barotrauma
public bool AllowVariants { get; set; }
public bool Equip { get; set; }
public bool Wear { get; set; }
public bool RequireLoaded { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; }
public bool SpeakIfFails { get; set; }
public InvSlotType? EquipSlotType { get; set; }
@@ -67,18 +74,41 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, string identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: this(character, new string[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { }
public AIObjectiveGetItem(Character character, string[] identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
public AIObjectiveGetItem(Character character, IEnumerable<string> identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
Equip = equip;
this.identifiersOrTags = identifiersOrTags;
this.spawnItemIfNotFound = spawnItemIfNotFound;
for (int i = 0; i < identifiersOrTags.Length; i++)
{
identifiersOrTags[i] = identifiersOrTags[i].ToLowerInvariant();
}
this.checkInventory = checkInventory;
this.identifiersOrTags = ParseGearTags(identifiersOrTags).ToImmutableArray();
ignoredIdentifiersOrTags = ParseIgnoredTags(identifiersOrTags).ToArray();
}
public static IEnumerable<string> ParseGearTags(IEnumerable<string> identifiersOrTags)
{
var tags = new List<string>();
foreach (string tag in identifiersOrTags)
{
if (!tag.Contains('!'))
{
tags.Add(tag.ToLowerInvariant());
}
}
return tags;
}
public static IEnumerable<string> ParseIgnoredTags(IEnumerable<string> identifiersOrTags)
{
var ignoredTags = new List<string>();
foreach (string tag in identifiersOrTags)
{
if (tag.Contains('!'))
{
ignoredTags.Add(tag.Remove("!").ToLowerInvariant());
}
}
return ignoredTags;
}
private bool CheckInventory()
@@ -219,6 +249,13 @@ namespace Barotrauma
}
else
{
if (!Equip)
{
// Try equipping and wearing the item
Wear = true;
Equip = true;
return;
}
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
@@ -243,6 +280,10 @@ namespace Barotrauma
{
// Try again
ignoredItems.Add(targetItem);
if (targetItem != moveToTarget && moveToTarget is Item item)
{
ignoredItems.Add(item);
}
ResetInternal();
}
else
@@ -269,7 +310,15 @@ namespace Barotrauma
}
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
bool checkPath = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.isFollowOrderObjective);
if (!CheckPathForEachItem)
{
// While following the player, let's ensure that there's a valid path to the target before accepting it.
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.isFollowOrderObjective);
}
bool checkPath = CheckPathForEachItem;
bool hasCalledPathFinder = false;
int itemsPerFrame = (int)priority;
for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++)
@@ -280,14 +329,20 @@ namespace Barotrauma
if (itemSub == null) { continue; }
Submarine mySub = character.Submarine;
if (mySub == null) { continue; }
if (!checkInventory)
{
// Ignore items in the inventory when defined not to check it.
if (item.IsOwnedBy(character)) { continue; }
}
if (!AllowStealing)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; }
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInCurrentOutpost) { continue; }
}
if (!CheckItem(item)) { continue; }
if (item.Container != null)
{
if (item.Container.HasTag("donttakeitems")) { continue; }
if (ignoredItems.Contains(item.Container)) { continue; }
if (ignoredContainerIdentifiers != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
@@ -315,17 +370,51 @@ namespace Barotrauma
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist));
float minDistFactor = EvaluateCombatPriority ? 0.1f : 0;
float distanceFactor = MathHelper.Lerp(1, minDistFactor, MathUtils.InverseLerp(100, 10000, dist));
itemPriority *= distanceFactor;
itemPriority *= item.Condition / item.MaxCondition;
if (EvaluateCombatPriority)
{
var mw = item.GetComponent<MeleeWeapon>();
var rw = item.GetComponent<RangedWeapon>();
float combatFactor = 0;
if (mw != null)
{
if (mw.CombatPriority > 0)
{
combatFactor = mw.CombatPriority / 100;
}
else
{
// The combat factor of items with zero combat priority is not allowed to be greater than 0.1f
combatFactor = Math.Min(AIObjectiveCombat.GetLethalDamage(mw) / 1000, 0.1f);
}
}
else if (rw != null)
{
if (rw.CombatPriority > 0)
{
combatFactor = rw.CombatPriority / 100;
}
else
{
combatFactor = Math.Min(AIObjectiveCombat.GetLethalDamage(rw) / 1000, 0.1f);
}
}
else
{
combatFactor = Math.Min(item.Components.Sum(ic => AIObjectiveCombat.GetLethalDamage(ic)) / 1000, 0.1f);
}
itemPriority *= combatFactor;
}
else
{
itemPriority *= item.Condition / item.MaxCondition;
}
// Ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
if (!hasCalledPathFinder && PathSteering != null && checkPath)
{
// While following the player, let's ensure that there's a valid path to the target before accepting it.
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
hasCalledPathFinder = true;
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable) { continue; }
@@ -355,7 +444,7 @@ namespace Barotrauma
targetItem = spawnedItem;
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInOutpost = true;
spawnedItem.SpawnedInCurrentOutpost = true;
}
});
}
@@ -365,7 +454,6 @@ namespace Barotrauma
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
#endif
SpeakCannotFind();
Abandon = true;
}
}
@@ -375,34 +463,19 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted) { return true; }
if (targetItem != null)
if (targetItem == null)
{
if (Equip && EquipSlotType.HasValue)
{
return character.HasEquippedItem(targetItem, EquipSlotType.Value);
}
else
{
return character.HasItem(targetItem, Equip);
}
}
else if (identifiersOrTags != null)
{
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (matchingItem != null)
{
if (Equip && EquipSlotType.HasValue)
{
return character.HasEquippedItem(matchingItem, EquipSlotType.Value);
}
else
{
return !Equip || character.HasEquippedItem(matchingItem);
}
}
// Not yet ready
return false;
}
return false;
if (Equip && EquipSlotType.HasValue)
{
return character.HasEquippedItem(targetItem, EquipSlotType.Value);
}
else
{
return character.HasItem(targetItem, Equip);
}
}
private bool CheckItem(Item item)
@@ -410,8 +483,10 @@ namespace Barotrauma
if (!item.IsInteractable(character)) { return false; }
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
if (ignoredItems.Contains(item)) { return false; };
if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.prefab.Identifier == id || item.HasTag(id))) { return false; }
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; }
return identifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id));
}
@@ -437,15 +512,20 @@ namespace Barotrauma
protected override void OnAbandon()
{
base.OnAbandon();
if (moveToTarget == null) { return; }
if (moveToTarget != null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
#endif
}
if (SpeakIfFails)
{
SpeakCannotFind();
}
}
private void SpeakCannotFind()
{
// TODO: Use the item name as the variable here.
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string msg = TextManager.Get("dialogcannotfinditem", true);
@@ -455,19 +535,5 @@ namespace Barotrauma
}
}
}
// TODO: remove?
private void SpeakCannotReach()
{
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString();
string msg = TargetName == null ? TextManager.Get("dialogcannotreachtarget", true) : TextManager.GetWithVariable("dialogcannotreachtarget", "[name]", TargetName, formatCapitals: !(moveToTarget is Character));
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotreachtarget", minDurationBetweenSimilar: 20.0f);
}
}
}
}
}
@@ -0,0 +1,91 @@
#nullable enable
using Barotrauma.Extensions;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma
{
class AIObjectiveGetItems : AIObjective
{
public override string Identifier { get; set; } = "get items";
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public bool AllowStealing { get; set; }
public bool TakeWholeStack { get; set; }
public bool AllowVariants { get; set; }
public bool Equip { get; set; }
public bool Wear { get; set; }
public bool CheckInventory { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; }
public bool RequireLoaded { get; set; }
private readonly ImmutableArray<string> gearTags;
private readonly string[] ignoredTags;
private bool subObjectivesCreated;
public readonly HashSet<Item> achievedItems = new HashSet<Item>();
public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable<string> identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
gearTags = AIObjectiveGetItem.ParseGearTags(identifiersOrTags).ToImmutableArray();
ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToArray();
}
protected override bool CheckObjectiveSpecific() => subObjectivesCreated && subObjectives.None();
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (!subObjectivesCreated)
{
foreach (string tag in gearTags)
{
AIObjectiveGetItem? getItem = null;
TryAddSubObjective(ref getItem, () =>
new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory)
{
AllowVariants = AllowVariants,
Wear = Wear,
TakeWholeStack = TakeWholeStack,
AllowStealing = AllowStealing,
ignoredIdentifiersOrTags = ignoredTags,
CheckPathForEachItem = CheckPathForEachItem,
RequireLoaded = RequireLoaded
},
onCompleted: () =>
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
achievedItems.Add(item);
}
},
onAbandon: () =>
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
});
}
subObjectivesCreated = true;
}
}
public override void Reset()
{
base.Reset();
subObjectivesCreated = false;
achievedItems.Clear();
}
}
}
@@ -27,6 +27,7 @@ namespace Barotrauma
public bool isFollowOrderObjective;
public bool mimic;
public bool SpeakIfFails { get; set; } = true;
public bool DebugLogWhenFails { get; set; } = true;
public bool UsePathingOutside { get; set; } = true;
public float extraDistanceWhileSwimming;
@@ -61,6 +62,9 @@ namespace Barotrauma
}
}
// TODO: Currently we never check the visibility (to the end node), which is actually unintentional.
// I don't think it has caused any issues so far, so let's keep defaulting to false for now, because the less we do raycasts the better.
// However, if there are cases where the bots attempt to go through walls (select the end node that is behind an obstacle), we should set this true.
public bool CheckVisibility { get; set; }
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
@@ -77,7 +81,7 @@ namespace Barotrauma
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public override bool AllowInAnySub => true;
public string DialogueIdentifier { get; set; }
public string DialogueIdentifier { get; set; } = "dialogcannotreachtarget";
public string TargetName { get; set; }
public ISpatialEntity Target { get; private set; }
@@ -149,7 +153,10 @@ namespace Barotrauma
private void SpeakCannotReach()
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
if (DebugLogWhenFails)
{
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
}
#endif
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails)
{
@@ -170,17 +177,12 @@ namespace Barotrauma
Abandon = true;
return;
}
if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
if (cannotFollow || Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
{
// Wait
character.AIController.SteeringManager.Reset();
return;
}
if (cannotFollow)
{
// Wait
character.AIController.SteeringManager.Reset();
}
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
@@ -211,26 +213,33 @@ namespace Barotrauma
}
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
bool isInside = character.CurrentHull != null;
bool targetIsOutside = (Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (isInside && targetIsOutside && !AllowGoingOutside)
bool hasOutdoorNodes = insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes;
if (isInside && hasOutdoorNodes && !AllowGoingOutside)
{
Abandon = true;
}
else if (HumanAIController.IsCurrentPathNullOrUnreachable)
else if (HumanAIController.SteeringManager == PathSteering)
{
waitUntilPathUnreachable -= deltaTime;
SteeringManager.Reset();
if (waitUntilPathUnreachable < 0)
if (HumanAIController.IsCurrentPathNullOrUnreachable)
{
SteeringManager.Reset();
if (waitUntilPathUnreachable < 0)
{
waitUntilPathUnreachable = pathWaitingTime;
if (repeat)
{
SpeakCannotReach();
}
else
{
Abandon = true;
}
}
}
else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false))
{
waitUntilPathUnreachable = pathWaitingTime;
if (repeat)
{
SpeakCannotReach();
}
else
{
Abandon = true;
}
}
}
if (!Abandon)
@@ -238,16 +247,16 @@ namespace Barotrauma
if (getDivingGearIfNeeded && !character.LockHands)
{
Character followTarget = Target as Character;
bool needsDivingSuit = !isInside || targetIsOutside;
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)) && character.NeedsAir;
if (mimic)
{
if (HumanAIController.HasDivingSuit(followTarget))
if (HumanAIController.HasDivingSuit(followTarget) && character.NeedsAir)
{
needsDivingGear = true;
needsDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget))
else if (HumanAIController.HasDivingMask(followTarget) && character.NeedsAir)
{
needsDivingGear = true;
}
@@ -323,7 +323,6 @@ namespace Barotrauma
SortObjectives();
}
private CoroutineHandle speakRoutine;
public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak)
{
if (character.IsDead)
@@ -379,6 +378,7 @@ namespace Barotrauma
var newCurrentOrder = CreateObjective(order, option, orderGiver);
if (newCurrentOrder != null)
{
newCurrentOrder.Abandoned += () => DismissSelf(order, option);
CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder));
}
if (!HasOrders())
@@ -386,53 +386,12 @@ namespace Barotrauma
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
CreateAutonomousObjectives();
}
else
else if (newCurrentOrder != null)
{
// This should be redundant, because all the objectives are reset when they are selected as active.
newCurrentOrder?.Reset();
if (speak && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
//if (speakRoutine != null)
//{
// CoroutineManager.StopCoroutines(speakRoutine);
//}
//speakRoutine = CoroutineManager.InvokeAfter(() =>
//{
// if (GameMain.GameSession == null || Level.Loaded == null) { return; }
// if (newCurrentOrder != null && character.SpeechImpediment < 100.0f)
// {
// if (newCurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
// }
// else if (newCurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
// }
// else if (newCurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
// }
// else if (newCurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
// }
// else if (newCurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
// }
// else if (newCurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
// }
// else if (newCurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
// }
// }
//}, 3);
string msg = newCurrentOrder.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative");
character.Speak(msg, delay: 1.0f);
}
}
}
@@ -465,7 +424,6 @@ namespace Barotrauma
break;
case "return":
newObjective = new AIObjectiveReturn(character, orderGiver, this, priorityModifier: priorityModifier);
newObjective.Abandoned += () => DismissSelf(order, option);
newObjective.Completed += () => DismissSelf(order, option);
break;
case "fixleaks":
@@ -491,12 +449,10 @@ namespace Barotrauma
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = true,
IsLoop = false,
Override = orderGiver != null && orderGiver.IsCommanding
};
// ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs,
// if we want that the bot just switches the pump on/off and continues doing something else.
// If we want that the bot does the objective and then forgets about it, I think we could do the same plus dismiss when the bot is done.
newObjective.Completed += () => DismissSelf(order, option);
}
else
{
@@ -566,6 +522,26 @@ namespace Barotrauma
case "escapehandcuffs":
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
break;
case "prepareforexpedition":
newObjective = new AIObjectivePrepare(character, this, order.TargetItems)
{
KeepActiveWhenReady = true,
CheckInventory = true,
Equip = false,
FindAllItems = true
};
break;
case "findweapon":
newObjective = new AIObjectivePrepare(character, this, order.TargetItems)
{
KeepActiveWhenReady = false,
CheckInventory = false,
Equip = true,
EvaluateCombatPriority = true,
FindAllItems = false
};
newObjective.Completed += () => DismissSelf(order, option);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
@@ -213,7 +213,6 @@ namespace Barotrauma
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = target.Item.Name,
endNodeFilter = node => node.Waypoint.Ladders == null
},
@@ -0,0 +1,150 @@
#nullable enable
using Barotrauma.Extensions;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma
{
class AIObjectivePrepare : AIObjective
{
public override string Identifier { get; set; } = "prepare";
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
private AIObjectiveGetItem? getSingleItemObjective;
private AIObjectiveGetItems? getMultipleItemsObjective;
private bool subObjectivesCreated;
private readonly ImmutableArray<string> gearTags;
private readonly HashSet<Item> items = new HashSet<Item>();
public bool KeepActiveWhenReady { get; set; }
public bool CheckInventory { get; set; }
public bool FindAllItems { get; set; }
public bool Equip { get; set; }
public bool EvaluateCombatPriority { get; set; }
private AIObjective? GetSubObjective() => getSingleItemObjective ?? getMultipleItemsObjective as AIObjective;
public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable<string> items, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
gearTags = items.ToImmutableArray();
}
protected override bool CheckObjectiveSpecific() => IsCompleted;
protected override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
Priority = objectiveManager.GetOrderPriority(this);
var subObjective = GetSubObjective();
if (subObjective != null && subObjective.IsCompleted)
{
Priority = 0;
items.RemoveWhere(i => i == null || i.Removed || !i.IsOwnedBy(character));
if (items.None())
{
Abandon = true;
}
else if (items.Any(i => i.Components.Any(i => !i.IsLoaded(character))))
{
Reset();
}
}
return Priority;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (!subObjectivesCreated)
{
if (FindAllItems)
{
if (!TryAddSubObjective(ref getMultipleItemsObjective, () => new AIObjectiveGetItems(character, objectiveManager, gearTags)
{
CheckInventory = CheckInventory,
Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority,
RequireLoaded = true
},
onCompleted: () =>
{
if (KeepActiveWhenReady)
{
if (getMultipleItemsObjective != null)
{
foreach (var item in getMultipleItemsObjective.achievedItems)
{
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
}
else
{
IsCompleted = true;
}
},
onAbandon: () => Abandon = true))
{
Abandon = true;
}
}
else
{
if (!TryAddSubObjective(ref getSingleItemObjective, () => new AIObjectiveGetItem(character, gearTags, objectiveManager, equip: Equip, checkInventory: CheckInventory)
{
EvaluateCombatPriority = EvaluateCombatPriority,
SpeakIfFails = true,
RequireLoaded = true
},
onCompleted: () =>
{
if (KeepActiveWhenReady)
{
if (getSingleItemObjective != null)
{
var item = getSingleItemObjective?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
else
{
IsCompleted = true;
}
},
onAbandon: () => Abandon = true))
{
Abandon = true;
}
}
subObjectivesCreated = true;
}
}
public override void Reset()
{
base.Reset();
items.Clear();
subObjectivesCreated = false;
RemoveSubObjective(ref getMultipleItemsObjective);
RemoveSubObjective(ref getSingleItemObjective);
}
}
}
@@ -107,7 +107,7 @@ namespace Barotrauma
{
foreach (RelatedItem requiredItem in kvp.Value)
{
var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true)
var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, equip: true)
{
AllowVariants = requiredItem.AllowVariants
};
@@ -219,8 +219,7 @@ namespace Barotrauma
{
// Don't stop in ladders, because we can't interact with other items while holding the ladders.
endNodeFilter = node => node.Waypoint.Ladders == null,
// Allow repairing hatches and airlock doors.
AllowGoingOutside = HumanAIController.ObjectiveManager.IsCurrentOrder<AIObjectiveRepairItems>() && Item.GetComponent<Door>() != null
TargetName = Item.Name
};
if (repairTool != null)
{
@@ -91,8 +91,7 @@ namespace Barotrauma
public static bool NearlyFullCondition(Item item)
{
float condition = item.ConditionPercentage;
return item.Repairables.All(r => condition >= r.RepairThreshold);
return item.Repairables.All(r => !r.IsBelowRepairThreshold);
}
protected override float TargetEvaluation()
@@ -319,9 +319,32 @@ namespace Barotrauma
{
itemListStr = itemNameList[0];
}
else if (itemNameList.Count == 2)
{
//[treatment1] or [treatment2]
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsLast",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemNameList[0], itemNameList[1] });
}
else
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
//[treatment1], [treatment2], [treatment3] ... or [treatmentx]
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsFirst",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemNameList[0], itemNameList[1] });
for (int i = 2; i < itemNameList.Count - 1; i++)
{
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsFirst",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemListStr, itemNameList[i] });
}
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsLast",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemListStr, itemNameList.Last() });
}
if (targetCharacter != character && character.IsOnPlayerTeam)
{
@@ -79,6 +79,7 @@ namespace Barotrauma
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target.IsInstigator) { return false; }
if (target.IsPet) { return false; }
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
@@ -170,6 +170,7 @@ namespace Barotrauma
public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1;
public bool IsPrefab { get; private set; }
public readonly bool MustManuallyAssign;
public readonly bool AutoDismiss;
public readonly OrderTarget TargetPosition;
@@ -363,6 +364,7 @@ namespace Barotrauma
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis";
DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false);
AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Movement);
}
/// <summary>
@@ -1048,7 +1048,8 @@ namespace Barotrauma
void UpdateClimbing()
{
if (character.SelectedConstruction == null || character.SelectedConstruction.GetComponent<Ladder>() == null || character.IsIncapacitated)
var ladder = character.SelectedConstruction?.GetComponent<Ladder>();
if (ladder == null || character.IsIncapacitated)
{
Anim = Animation.None;
return;
@@ -1075,22 +1076,29 @@ namespace Barotrauma
if (leftHand == null || rightHand == null || head == null || torso == null) { return; }
Vector2 ladderSimPos = ConvertUnits.ToSimUnits(
character.SelectedConstruction.Rect.X + character.SelectedConstruction.Rect.Width / 2.0f,
character.SelectedConstruction.Rect.Y);
ladder.Item.Rect.X + ladder.Item.Rect.Width / 2.0f,
ladder.Item.Rect.Y);
Vector2 ladderSimSize = ConvertUnits.ToSimUnits(character.SelectedConstruction.Rect.Size.ToVector2());
Vector2 ladderSimSize = ConvertUnits.ToSimUnits(ladder.Item.Rect.Size.ToVector2());
float lowestLadderSimPos = ladderSimPos.Y - ladderSimPos.Y;
var lowestNearbyLadder = GetLowestNearbyLadder(ladder);
if (lowestNearbyLadder != null && lowestNearbyLadder != ladder)
{
ladderSimSize.Y = ConvertUnits.ToSimUnits(ladder.Item.WorldRect.Y - (lowestNearbyLadder.Item.WorldRect.Y - lowestNearbyLadder.Item.Rect.Size.Y));
}
float stepHeight = ConvertUnits.ToSimUnits(30.0f);
if (currentHull == null && character.SelectedConstruction.Submarine != null)
if (currentHull == null && ladder.Item.Submarine != null)
{
ladderSimPos += character.SelectedConstruction.Submarine.SimPosition;
ladderSimPos += ladder.Item.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && currentHull.Submarine != character.SelectedConstruction.Submarine && character.SelectedConstruction.Submarine != null)
else if (currentHull?.Submarine != null && currentHull.Submarine != ladder.Item.Submarine && ladder.Item.Submarine != null)
{
ladderSimPos += character.SelectedConstruction.Submarine.SimPosition - currentHull.Submarine.SimPosition;
ladderSimPos += ladder.Item.Submarine.SimPosition - currentHull.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && character.SelectedConstruction.Submarine == null)
else if (currentHull?.Submarine != null && ladder.Item.Submarine == null)
{
ladderSimPos -= currentHull.Submarine.SimPosition;
}
@@ -1174,25 +1182,35 @@ namespace Barotrauma
float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI;
movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor));
Vector2 subSpeed = currentHull != null || character.SelectedConstruction.Submarine == null
? Vector2.Zero : character.SelectedConstruction.Submarine.Velocity;
Vector2 subSpeed = currentHull != null || ladder.Item.Submarine == null
? Vector2.Zero : ladder.Item.Submarine.Velocity;
Vector2 climbForce = new Vector2(0.0f, movement.Y + 0.3f) * movementFactor;
//reached the top of the ladders -> can't go further up
if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); }
//reached the bottom -> can't go further down
float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.height;
if (floorFixture != null &&
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) &&
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) &&
character.SimPosition.Y < standOnFloorY + minHeightFromFloor)
{
climbForce.Y = MathHelper.Clamp((standOnFloorY + minHeightFromFloor - character.SimPosition.Y) * 5.0f, climbForce.Y, 1.0f);
}
//apply forces to the collider to move the Character up/down
Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass);
float movementMultiplier = targetMovement.Y < 0 ? 0 : 1;
head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque);
if (!character.SelectedConstruction.Prefab.Triggers.Any())
if (!ladder.Item.Prefab.Triggers.Any())
{
character.SelectedConstruction = null;
return;
}
Rectangle trigger = character.SelectedConstruction.Prefab.Triggers.FirstOrDefault();
trigger = character.SelectedConstruction.TransformTrigger(trigger);
Rectangle trigger = ladder.Item.Prefab.Triggers.FirstOrDefault();
trigger = ladder.Item.TransformTrigger(trigger);
bool isRemote = false;
bool isClimbing = true;
@@ -1221,6 +1239,19 @@ namespace Barotrauma
character.SelectedConstruction = null;
IgnorePlatforms = false;
}
Ladder GetLowestNearbyLadder(Ladder currentLadder, float threshold = 16.0f)
{
foreach (Ladder ladder in Ladder.List)
{
if (ladder == currentLadder || !ladder.Item.IsInteractable(character)) { continue; }
if (Math.Abs(ladder.Item.WorldPosition.X - currentLadder.Item.WorldPosition.X) > threshold) { continue; }
if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y) { continue; }
if ((currentLadder.Item.WorldRect.Y - currentLadder.Item.Rect.Height) - ladder.Item.WorldRect.Y > threshold) { continue; }
return ladder;
}
return null;
}
}
void UpdateDying(float deltaTime)
@@ -1576,6 +1607,7 @@ namespace Barotrauma
Vector2 shoulderPos = rightShoulder.WorldAnchorA;
Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY;
if (!MathUtils.IsValid(dragDir)) { dragDir = Vector2.UnitY; }
targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength);
targetForce = 200.0f;
@@ -121,6 +121,7 @@ namespace Barotrauma
protected Vector2 overrideTargetMovement;
protected float floorY, standOnFloorY;
protected Fixture floorFixture;
protected Vector2 floorNormal = Vector2.UnitY;
protected float surfaceY;
@@ -488,6 +489,7 @@ namespace Barotrauma
limbDictionary = new Dictionary<LimbType, Limb>();
limbs = new Limb[RagdollParams.Limbs.Count];
RagdollParams.Limbs.ForEach(l => AddLimb(l));
if (limbs.Contains(null)) { return; }
SetupDrawOrder();
}
@@ -548,19 +550,23 @@ namespace Barotrauma
byte ID = Convert.ToByte(limbParams.ID);
Limb limb = new Limb(this, character, limbParams);
limb.body.FarseerBody.OnCollision += OnLimbCollision;
if (ID >= Limbs.Length)
{
throw new Exception($"Failed to add a limb to the character \"{Character?.ConfigPath ?? "null"}\" (limb index {ID} out of bounds). The ragdoll file may be configured incorrectly.");
}
Limbs[ID] = limb;
Mass += limb.Mass;
if (!limbDictionary.ContainsKey(limb.type)) limbDictionary.Add(limb.type, limb);
if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); }
}
public void AddLimb(Limb limb)
{
if (Limbs.Contains(limb)) return;
if (Limbs.Contains(limb)) { return; }
limb.body.FarseerBody.OnCollision += OnLimbCollision;
Array.Resize(ref limbs, Limbs.Length + 1);
Limbs[Limbs.Length - 1] = limb;
Mass += limb.Mass;
if (!limbDictionary.ContainsKey(limb.type)) limbDictionary.Add(limb.type, limb);
if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); }
SetupDrawOrder();
}
@@ -923,8 +929,8 @@ namespace Barotrauma
}
Hull newHull = Hull.FindHull(findPos, currentHull);
if (newHull == currentHull) return;
if (newHull == currentHull) { return; }
if (!CanEnterSubmarine || (character.AIController != null && !character.AIController.CanEnterSubmarine))
{
@@ -957,16 +963,16 @@ namespace Barotrauma
if (newHull?.Submarine == null && currentHull?.Submarine != null)
{
//don't teleport out yet if the character is going through a gap
if (Gap.FindAdjacent(currentHull.ConnectedGaps, findPos, 150.0f) != null) { return; }
if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f) != null) { return; }
if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine())) != null)) { return; }
character.MemLocalState?.Clear();
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity);
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity, detachProjectiles: false);
}
//out -> in
else if (currentHull == null && newHull.Submarine != null)
{
character.MemLocalState?.Clear();
Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity);
Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity, detachProjectiles: false);
}
//from one sub to another
else if (newHull != null && currentHull != null && newHull.Submarine != currentHull.Submarine)
@@ -974,13 +980,13 @@ namespace Barotrauma
character.MemLocalState?.Clear();
Vector2 newSubPos = newHull.Submarine == null ? Vector2.Zero : newHull.Submarine.Position;
Vector2 prevSubPos = currentHull.Submarine == null ? Vector2.Zero : currentHull.Submarine.Position;
Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero);
Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero, detachProjectiles: false);
}
}
CurrentHull = newHull;
character.Submarine = currentHull?.Submarine;
character.AttachedProjectiles.ForEach(p => p?.Item?.UpdateTransform());
}
private void PreventOutsideCollision()
@@ -1013,7 +1019,7 @@ namespace Barotrauma
}
}
public void Teleport(Vector2 moveAmount, Vector2 velocityChange)
public void Teleport(Vector2 moveAmount, Vector2 velocityChange, bool detachProjectiles = true)
{
foreach (Limb limb in Limbs)
{
@@ -1036,7 +1042,7 @@ namespace Barotrauma
character.DisableImpactDamageTimer = 0.25f;
SetPosition(Collider.SimPosition + moveAmount);
SetPosition(Collider.SimPosition + moveAmount, detachProjectiles: detachProjectiles);
character.CursorPosition += moveAmount;
Collider?.UpdateDrawPosition();
@@ -1500,6 +1506,7 @@ namespace Barotrauma
{
onGround = false;
Stairs = null;
floorFixture = null;
Vector2 rayStart = simPosition;
float height = ColliderHeightFromFloor;
if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); }
@@ -1572,6 +1579,7 @@ namespace Barotrauma
if (standOnFloorFixture != null && !IsHanging)
{
floorFixture = standOnFloorFixture;
standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction;
if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f)
{
@@ -1612,7 +1620,7 @@ namespace Barotrauma
}
}
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false)
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true)
{
if (!MathUtils.IsValid(simPosition))
{
@@ -1625,13 +1633,28 @@ namespace Barotrauma
}
if (MainLimb == null) { return; }
// A Work-around for an issue with teleporting the characters:
// Detach every latcher when either one of the latchers or the target is teleported,
// because otherwise all the characters are teleported to invalid positions.
if (Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached)
{
var target = enemyAI.LatchOntoAI.TargetCharacter;
if (target != null)
{
target.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
target.Latchers.Clear();
}
enemyAI.LatchOntoAI.DeattachFromBody(reset: true);
}
Character.Latchers.ForEachMod(l => l.DeattachFromBody(reset: true));
Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
Character.Latchers.Clear();
if (detachProjectiles)
{
character.AttachedProjectiles.ForEachMod(p => p?.Unstick());
character.AttachedProjectiles.Clear();
}
Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition;
if (lerp)
{
@@ -1712,7 +1735,7 @@ namespace Barotrauma
if (distSqrd > resetDist * resetDist)
{
//ragdoll way too far, reset position
SetPosition(Collider.SimPosition, true, forceMainLimbToCollider: true);
SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true);
}
if (distSqrd > allowedDist * allowedDist)
{
@@ -1732,7 +1755,7 @@ namespace Barotrauma
else if (collisionsDisabled)
{
//set the position of the ragdoll to make sure limbs don't get stuck inside walls when re-enabling collisions
SetPosition(Collider.SimPosition, true);
SetPosition(Collider.SimPosition, lerp: true);
collisionsDisabled = false;
//force collision categories to be updated
prevCollisionCategory = Category.None;
@@ -130,6 +130,7 @@ namespace Barotrauma
}
public readonly HashSet<LatchOntoAI> Latchers = new HashSet<LatchOntoAI>();
public readonly HashSet<Projectile> AttachedProjectiles = new HashSet<Projectile>();
protected readonly Dictionary<string, ActiveTeamChange> activeTeamChanges = new Dictionary<string, ActiveTeamChange>();
protected ActiveTeamChange currentTeamChange;
@@ -265,7 +266,7 @@ namespace Barotrauma
private CharacterPrefab prefab;
public readonly CharacterParams Params;
public string SpeciesName => Params.SpeciesName;
public string SpeciesName => Params?.SpeciesName ?? "null";
public string Group => Params.Group;
public bool IsHumanoid => Params.Humanoid;
public bool IsHusk => Params.Husk;
@@ -2640,7 +2641,7 @@ namespace Barotrauma
ApplyStatusEffects(ActionType.Always, deltaTime);
PreviousHull = CurrentHull;
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, true);
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, useWorldCoordinates: true);
speechBubbleTimer = Math.Max(0.0f, speechBubbleTimer - deltaTime);
@@ -2710,7 +2711,7 @@ namespace Barotrauma
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Implode();
if (IsDead) { return; }
if (IsDead) { return; }
}
}
}
@@ -2719,7 +2720,9 @@ namespace Barotrauma
PressureTimer = 0.0f;
}
}
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && WorldPosition.Y < CharacterHealth.CrushDepth)
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) &&
PressureProtection < (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f) &&
WorldPosition.Y < CharacterHealth.CrushDepth)
{
//implode if below crush depth, and either outside or in a high-pressure hull
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
@@ -3124,47 +3127,53 @@ namespace Barotrauma
//set the character order only if the character is close enough to hear the message
if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
if (order != null && order.OrderGiver != orderGiver)
if (order != null)
{
order.OrderGiver = orderGiver;
}
switch (order?.Category)
{
case OrderCategory.Operate when order?.TargetEntity != null:
// If there's another character operating the same device, make them dismiss themself
foreach (var character in CharacterList)
if (order.OrderGiver != orderGiver)
{
order.OrderGiver = orderGiver;
}
if (order.AutoDismiss)
{
switch (order.Category)
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!(character.AIController is HumanAIController)) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
case OrderCategory.Operate when order.TargetEntity != null:
// If there's another character operating the same device, make them dismiss themself
foreach (var character in CharacterList)
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!(character.AIController is HumanAIController)) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
if (!currentOrder.Order.AutoDismiss) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
break;
}
}
break;
case OrderCategory.Movement:
// If there character has another movement order, dismiss that order
OrderInfo? orderToReplace = null;
foreach (var currentOrder in CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Movement) { continue; }
orderToReplace = currentOrder;
break;
}
if (orderToReplace.HasValue && orderToReplace.Value.Order.AutoDismiss)
{
SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force);
}
break;
}
}
break;
case OrderCategory.Movement:
// If there character has another movement order, dismiss that order
OrderInfo? orderToReplace = null;
foreach (var currentOrder in CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Movement) { continue; }
orderToReplace = currentOrder;
break;
}
if (orderToReplace.HasValue)
{
SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force);
}
break;
}
}
// Prevent adding duplicate orders
@@ -3624,7 +3633,7 @@ namespace Barotrauma
{
if (attacker.TeamID == TeamID)
{
afflictions = afflictions.Where(a => !a.Prefab.IsBuff);
afflictions = afflictions.Where(a => a.Prefab.IsBuff);
if (!afflictions.Any()) { return new AttackResult(); }
}
}
@@ -3936,6 +3945,7 @@ namespace Barotrauma
}
SelectedConstruction = null;
SelectedCharacter = null;
AnimController.ResetPullJoints();
@@ -4063,10 +4073,11 @@ namespace Barotrauma
public void TeleportTo(Vector2 worldPos)
{
CurrentHull = null;
AnimController.CurrentHull = null;
Submarine = null;
AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), false);
AnimController.FindHull(worldPos, true);
AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), lerp: false);
AnimController.FindHull(worldPos, setSubmarine: true);
}
public static void SaveInventory(Inventory inventory, XElement parentElement)
@@ -1176,7 +1176,7 @@ namespace Barotrauma
int salary = 0;
foreach (Skill skill in Job.Skills)
{
salary += (int)(skill.Level * skill.Prefab.PriceMultiplier);
salary += (int)(skill.Level * skill.PriceMultiplier);
}
return (int)(salary * Job.Prefab.PriceMultiplier);
@@ -1274,19 +1274,18 @@ namespace Barotrauma
return Math.Max(GetTotalTalentPoints() - GetUnlockedTalentsInTree().Count(), 0);
}
public float GetProgressTowardsNextLevel()
public int GetProgressTowardsNextLevel()
{
float progress = (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel());
return progress;
return (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel());
}
public float GetExperienceRequiredForCurrentLevel()
public int GetExperienceRequiredForCurrentLevel()
{
GetCurrentLevel(out int experienceRequired);
return experienceRequired;
}
public float GetExperienceRequiredToLevelUp()
public int GetExperienceRequiredToLevelUp()
{
int level = GetCurrentLevel(out int experienceRequired);
return experienceRequired + ExperienceRequiredPerLevel(level);
@@ -174,7 +174,8 @@ namespace Barotrauma
private void CharacterDead(Character character, CauseOfDeath causeOfDeath)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Strength < TransformThresholdOnDeath || character.Removed)
if (Strength < TransformThresholdOnDeath || character.Removed ||
character.CharacterHealth.GetAllAfflictions().Any(a => a.GetActiveEffect()?.BlockTransformation.Contains(Prefab.Identifier) ?? false))
{
UnsubscribeFromDeathEvent();
return;
@@ -193,7 +194,7 @@ namespace Barotrauma
CoroutineManager.StartCoroutine(CreateAIHusk());
}
private IEnumerable<object> CreateAIHusk()
private IEnumerable<CoroutineStatus> CreateAIHusk()
{
//character already in remove queue (being removed by something else, for example a modded affliction that uses AfflictionHusk as the base)
// -> don't spawn the AI husk
@@ -234,6 +234,11 @@ namespace Barotrauma
[Serialize("0,0,0,0", false)]
public Color MaxBodyTint { get; private set; }
/// <summary>
/// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character
/// </summary>
public string[] BlockTransformation { get; private set; }
public readonly Dictionary<StatTypes, (float minValue, float maxValue)> AfflictionStatValues = new Dictionary<StatTypes, (float minValue, float maxValue)>();
public readonly HashSet<AbilityFlags> AfflictionAbilityFlags = new HashSet<AbilityFlags>();
@@ -245,6 +250,7 @@ namespace Barotrauma
SerializableProperty.DeserializeProperties(this, element);
resistanceFor = element.GetAttributeStringArray("resistancefor", new string[0], convertToLowerInvariant: true);
BlockTransformation = element.GetAttributeStringArray("blocktransformation", new string[0], convertToLowerInvariant: true);
foreach (XElement subElement in element.Elements())
{
@@ -153,7 +153,10 @@ namespace Barotrauma
(GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, CampaignInteractionType);
if (positionToStayIn != null && humanAI != null)
{
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200));
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)
{
DebugLogWhenFails = false
});
}
}
}
@@ -9,7 +9,7 @@ namespace Barotrauma
{
private readonly JobPrefab prefab;
private Dictionary<string, Skill> skills;
private readonly Dictionary<string, Skill> skills;
public string Name
{
@@ -282,7 +282,7 @@ namespace Barotrauma
Variants = variant;
Skills.Sort((x,y) => y.LevelRange.X.CompareTo(x.LevelRange.X));
Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start));
// Disabled on purpose, TODO: remove all references?
//ClothingElement = element.GetChildElement("PortraitClothing");
@@ -34,14 +34,14 @@ namespace Barotrauma
}
}
internal SkillPrefab Prefab { get; private set; }
public readonly float PriceMultiplier = 1.0f;
public Skill(SkillPrefab prefab)
{
this.Prefab = prefab;
Identifier = prefab.Identifier;
level = Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y, Rand.RandSync.Server);
level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, Rand.RandSync.Server);
icon = GetIcon();
PriceMultiplier = prefab.PriceMultiplier;
}
public Skill(string identifier, float level)
@@ -7,7 +7,7 @@ namespace Barotrauma
{
public readonly string Identifier;
public Vector2 LevelRange { get; private set; }
public Range<float> LevelRange { get; private set; }
/// <summary>
/// How much this skill affects characters' hiring cost
@@ -23,12 +23,13 @@ namespace Barotrauma
var levelString = element.GetAttributeString("level", "");
if (levelString.Contains(","))
{
LevelRange = XMLExtensions.ParseVector2(levelString, false);
var rangeVector2 = XMLExtensions.ParseVector2(levelString, false);
LevelRange = new Range<float>(rangeVector2.X, rangeVector2.Y);
}
else
{
float skillLevel = float.Parse(levelString, System.Globalization.CultureInfo.InvariantCulture);
LevelRange = new Vector2(skillLevel, skillLevel);
LevelRange = new Range<float>(skillLevel, skillLevel);
}
IsPrimarySkill = element.GetAttributeBool("primary", false);
@@ -430,8 +430,15 @@ namespace Barotrauma
public float Dir
{
get { return ((dir == Direction.Left) ? -1.0f : 1.0f); }
set { dir = (value == -1.0f) ? Direction.Left : Direction.Right; }
get { return (dir == Direction.Left) ? -1.0f : 1.0f; }
set
{
dir = (value == -1.0f) ? Direction.Left : Direction.Right;
if (body != null)
{
body.Dir = Dir;
}
}
}
public int RefJointIndex => Params.RefJoint;
@@ -16,10 +16,12 @@ namespace Barotrauma.Abilities
private readonly string itemIdentifier;
private readonly string[] tags;
private readonly WeaponType weapontype;
private bool ignoreNonHarmfulAttacks;
public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
itemIdentifier = conditionElement.GetAttributeString("itemidentifier", "");
tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true);
ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false);
switch (conditionElement.GetAttributeString("weapontype", ""))
{
case "melee":
@@ -35,8 +37,15 @@ namespace Barotrauma.Abilities
{
if (abilityObject is AbilityAttackData attackData)
{
Item item = attackData?.SourceAttack?.SourceItem;
if (ignoreNonHarmfulAttacks && attackData.SourceAttack != null)
{
if (attackData.SourceAttack.Stun <= 0.0f && (attackData.SourceAttack.Afflictions?.All(a => a.Key.Prefab.IsBuff) ?? true))
{
return false;
}
}
Item item = attackData?.SourceAttack?.SourceItem;
if (item == null)
{
DebugConsole.AddWarning($"Source Item was not found in {this} for talent {characterTalent.DebugIdentifier}!");
@@ -61,10 +70,13 @@ namespace Barotrauma.Abilities
switch (weapontype)
{
// it is possible that an item that has both a melee and a projectile component will return true
// even when not used as a melee/ranged weapon respectively
// attackdata should contain data regarding whether the attack is melee or not
case WeaponType.Melee:
return item.GetComponent<MeleeWeapon>() != null;
case WeaponType.Ranged:
return item.GetComponent<RangedWeapon>() != null;
return item.GetComponent<Projectile>() != null;
}
return true;
@@ -18,13 +18,13 @@ namespace Barotrauma.Abilities
protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData)
{
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject}");
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}");
}
protected abstract bool MatchesConditionSpecific(AbilityObject abilityObject);
public override bool MatchesCondition()
{
DebugConsole.ThrowError("Used data-reliant ability condition in a state-based ability! This is not allowed.");
DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed.");
return false;
}
public override bool MatchesCondition(AbilityObject abilityObject)
@@ -79,17 +79,17 @@ namespace Barotrauma.Abilities
protected virtual void ApplyEffect()
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect");
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
}
protected virtual void ApplyEffect(AbilityObject abilityObject)
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect");
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
}
protected void LogabilityObjectMismatch()
{
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type.");
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}");
}
// XML
@@ -10,8 +10,10 @@ namespace Barotrauma.Abilities
protected readonly List<StatusEffect> statusEffects;
private readonly bool applyToSelf;
private readonly bool nearbyCharactersAppliesToSelf;
private readonly bool nearbyCharactersAppliesToAllies;
private readonly bool nearbyCharactersAppliesToEnemies;
private readonly bool applyToSelected;
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
@@ -19,9 +21,11 @@ namespace Barotrauma.Abilities
public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
applyToSelf = abilityElement.GetAttributeBool("applytoself", false);
applyToSelected = abilityElement.GetAttributeBool("applytoselected", false);
nearbyCharactersAppliesToSelf = abilityElement.GetAttributeBool("nearbycharactersappliestoself", true);
nearbyCharactersAppliesToAllies = abilityElement.GetAttributeBool("nearbycharactersappliestoallies", true);
nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true);
}
protected void ApplyEffectSpecific(Character targetCharacter)
@@ -46,6 +50,10 @@ namespace Barotrauma.Abilities
{
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
}
if (!nearbyCharactersAppliesToEnemies)
{
targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character));
}
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
}
@@ -75,7 +83,7 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter)
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter && !applyToSelf)
{
ApplyEffectSpecific(targetCharacter);
}
@@ -31,5 +31,10 @@ namespace Barotrauma.Abilities
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffect();
}
}
}
@@ -5,7 +5,7 @@ namespace Barotrauma.Abilities
{
class CharacterAbilityModifyAttackData : CharacterAbility
{
private readonly List<Affliction> afflictions;
private readonly List<Affliction> afflictions = new List<Affliction>();
private readonly float addedDamageMultiplier;
private readonly float addedPenetration;
@@ -1,4 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
@@ -15,14 +16,19 @@ namespace Barotrauma.Abilities
if (!TalentTree.JobTalentTrees.TryGetValue(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; }
var subTree = talentTree.TalentSubTrees.Find(t => t.TalentOptionStages.Any(ts => ts.Talents.Contains(CharacterTalent.Prefab)));
if (subTree != null)
{
subTree.ForceUnlock = true;
foreach (var talentOption in subTree.TalentOptionStages)
{
foreach (var talent in talentOption.Talents)
{
if (talent == CharacterTalent.Prefab) { continue; }
Character.GiveTalent(talent);
if (Character.GiveTalent(talent))
{
Character.Info.AdditionalTalentPoints++;
}
}
}
}
@@ -101,37 +101,40 @@ namespace Barotrauma
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var rootElement = doc.Root;
switch (rootElement.Name.ToString().ToLowerInvariant())
void loadSinglePrefab(XElement element, bool isOverride)
{
case "talent":
TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), false);
break;
case "talents":
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
var itemElement = element.GetChildElement("talent");
if (itemElement != null)
{
TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), true);
}
else
{
DebugConsole.ThrowError($"Cannot find a talent element from the children of the override element defined in {file.Path}");
}
}
else
{
TalentPrefabs.Add(new TalentPrefab(element, file.Path), false);
}
}
break;
default:
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}");
break;
TalentPrefabs.Add(new TalentPrefab(element, file.Path) { ContentPackage = file.ContentPackage }, isOverride);
}
void loadMultiplePrefabs(XElement element, bool isOverride)
{
foreach (var subElement in element.Elements())
{
interpretElement(subElement, isOverride);
}
}
void interpretElement(XElement subElement, bool isOverride)
{
if (subElement.IsOverride())
{
loadMultiplePrefabs(subElement, true);
}
else if (subElement.Name.LocalName.Equals("talents", StringComparison.OrdinalIgnoreCase))
{
loadMultiplePrefabs(subElement, isOverride);
}
else if (subElement.Name.LocalName.Equals("talent", StringComparison.OrdinalIgnoreCase))
{
loadSinglePrefab(subElement, isOverride);
}
else
{
DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentPrefab)} prefab type: '{subElement.Name}' in {file.Path}");
}
}
interpretElement(doc.Root, false);
}
public static void LoadAll(IEnumerable<ContentFile> files)
@@ -5,7 +5,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
class TalentTree
class TalentTree : IPrefab, IDisposable
{
public enum TalentTreeStageState
{
@@ -16,7 +16,7 @@ namespace Barotrauma
Highlighted
}
public static readonly Dictionary<string, TalentTree> JobTalentTrees = new Dictionary<string, TalentTree>();
public static readonly PrefabCollection<TalentTree> JobTalentTrees = new PrefabCollection<TalentTree>();
public readonly List<TalentSubTree> TalentSubTrees = new List<TalentSubTree>();
@@ -26,13 +26,21 @@ namespace Barotrauma
private set;
}
public string OriginalName => Identifier;
public string Identifier { get; }
public string FilePath { get; }
public ContentPackage ContentPackage { get; set; }
public TalentTree(XElement element, string filePath)
{
ConfigElement = element;
FilePath = filePath;
Identifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant();
string jobIdentifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant();
if (string.IsNullOrEmpty(jobIdentifier))
if (string.IsNullOrEmpty(Identifier))
{
DebugConsole.ThrowError($"No job defined for talent tree in \"{filePath}\"!");
return;
@@ -50,20 +58,15 @@ namespace Barotrauma
TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talent, StringComparison.OrdinalIgnoreCase));
if (talentPrefab == null)
{
DebugConsole.AddWarning($"Talent tree for job {jobIdentifier} contains non-existent talent {talent}! Talent tree not added.");
DebugConsole.AddWarning($"Talent tree for job {Identifier} contains non-existent talent {talent}! Talent tree not added.");
return;
}
if (!duplicateSet.Add(talent))
{
DebugConsole.ThrowError($"Talent tree for job {jobIdentifier} contains duplicate talent {talent}! Talent tree not added.");
DebugConsole.ThrowError($"Talent tree for job {Identifier} contains duplicate talent {talent}! Talent tree not added.");
return;
}
}
if (!JobTalentTrees.TryAdd(jobIdentifier, this))
{
DebugConsole.ThrowError($"Could not add talent tree for job {jobIdentifier}! A talent tree for this job is already likely defined");
}
}
public bool TalentIsInTree(string talentIdentifier)
@@ -78,37 +81,40 @@ namespace Barotrauma
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var rootElement = doc.Root;
switch (rootElement.Name.ToString().ToLowerInvariant())
void loadSinglePrefab(XElement element, bool isOverride)
{
case "talenttree":
new TalentTree(rootElement, file.Path);
break;
case "talenttrees":
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
var treeElement = element.GetChildElement("talenttree");
if (treeElement != null)
{
new TalentTree(rootElement, file.Path);
}
else
{
DebugConsole.ThrowError($"Cannot find a talent tree element from the children of the override element defined in {file.Path}");
}
}
else
{
new TalentTree(element, file.Path);
}
}
break;
default:
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}");
break;
JobTalentTrees.Add(new TalentTree(element, file.Path) { ContentPackage = file.ContentPackage }, isOverride);
}
void loadMultiplePrefabs(XElement element, bool isOverride)
{
foreach (var subElement in element.Elements())
{
interpretElement(subElement, isOverride);
}
}
void interpretElement(XElement subElement, bool isOverride)
{
if (subElement.IsOverride())
{
loadMultiplePrefabs(subElement, true);
}
else if (subElement.Name.LocalName.Equals("talenttrees", StringComparison.OrdinalIgnoreCase))
{
loadMultiplePrefabs(subElement, isOverride);
}
else if (subElement.Name.LocalName.Equals("talenttree", StringComparison.OrdinalIgnoreCase))
{
loadSinglePrefab(subElement, isOverride);
}
else
{
DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentTree)} prefab type: '{subElement.Name}' in {file.Path}");
}
}
interpretElement(doc.Root, false);
}
public static void LoadAll(IEnumerable<ContentFile> files)
@@ -190,6 +196,8 @@ namespace Barotrauma
foreach (var subTree in talentTree.TalentSubTrees)
{
if (subTree.ForceUnlock && subTree.TalentOptionStages.Any(option => option.Talents.Any(t => t.Identifier == talentIdentifier))) { return true; }
foreach (var talentOptionStage in subTree.TalentOptionStages)
{
bool hasTalentInThisTier = talentOptionStage.Talents.Any(t => selectedTalents.Contains(t.Identifier));
@@ -220,7 +228,7 @@ namespace Barotrauma
canStillUnlock = false;
foreach (string talent in selectedTalents)
{
if (IsViableTalentForCharacter(controlledCharacter, talent, viableTalents))
if (!viableTalents.Contains(talent) && IsViableTalentForCharacter(controlledCharacter, talent, viableTalents))
{
viableTalents.Add(talent);
canStillUnlock = true;
@@ -229,6 +237,14 @@ namespace Barotrauma
}
return viableTalents;
}
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
JobTalentTrees.Remove(this);
}
}
class TalentSubTree
@@ -237,6 +253,8 @@ namespace Barotrauma
public string DisplayName { get; }
public bool ForceUnlock;
public readonly List<TalentOption> TalentOptionStages = new List<TalentOption>();
public TalentSubTree(XElement subTreeElement)
@@ -5,14 +5,64 @@ using System.Threading;
namespace Barotrauma
{
enum CoroutineStatus
abstract class CoroutineStatus
{
Running, Success, Failure
public static CoroutineStatus Running => EnumCoroutineStatus.Running;
public static CoroutineStatus Success => EnumCoroutineStatus.Success;
public static CoroutineStatus Failure => EnumCoroutineStatus.Failure;
public abstract bool CheckFinished(float deltaTime);
public abstract bool EndsCoroutine(CoroutineHandle handle);
}
class EnumCoroutineStatus : CoroutineStatus
{
private enum StatusValue
{
Running, Success, Failure
}
private readonly StatusValue Value;
private EnumCoroutineStatus(StatusValue value) { Value = value; }
public new readonly static EnumCoroutineStatus Running = new EnumCoroutineStatus(StatusValue.Running);
public new readonly static EnumCoroutineStatus Success = new EnumCoroutineStatus(StatusValue.Success);
public new readonly static EnumCoroutineStatus Failure = new EnumCoroutineStatus(StatusValue.Failure);
public override bool CheckFinished(float deltaTime)
{
return true;
}
public override bool EndsCoroutine(CoroutineHandle handle)
{
if (Value == StatusValue.Failure)
{
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
}
return Value != StatusValue.Running;
}
public override bool Equals(object obj)
{
return obj is EnumCoroutineStatus other && Value == other.Value;
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override string ToString()
{
return Value.ToString();
}
}
class CoroutineHandle
{
public readonly IEnumerator<object> Coroutine;
public readonly IEnumerator<CoroutineStatus> Coroutine;
public readonly string Name;
public Exception Exception;
@@ -20,7 +70,7 @@ namespace Barotrauma
public Thread Thread;
public CoroutineHandle(IEnumerator<object> coroutine, string name = "", bool useSeparateThread = false)
public CoroutineHandle(IEnumerator<CoroutineStatus> coroutine, string name = "", bool useSeparateThread = false)
{
Coroutine = coroutine;
Name = string.IsNullOrWhiteSpace(name) ? coroutine.ToString() : name;
@@ -36,7 +86,7 @@ namespace Barotrauma
public static float UnscaledDeltaTime, DeltaTime;
public static CoroutineHandle StartCoroutine(IEnumerable<object> func, string name = "", bool useSeparateThread = false)
public static CoroutineHandle StartCoroutine(IEnumerable<CoroutineStatus> func, string name = "", bool useSeparateThread = false)
{
var handle = new CoroutineHandle(func.GetEnumerator(), name);
lock (Coroutines)
@@ -63,7 +113,7 @@ namespace Barotrauma
return StartCoroutine(DoInvokeAfter(action, delay));
}
private static IEnumerable<object> DoInvokeAfter(Action action, float delay)
private static IEnumerable<CoroutineStatus> DoInvokeAfter(Action action, float delay)
{
if (action == null)
{
@@ -127,9 +177,7 @@ namespace Barotrauma
bool joined = false;
while (!joined)
{
#if CLIENT
CrossThread.ProcessTasks();
#endif
joined = coroutine.Thread.Join(TimeSpan.FromMilliseconds(500));
}
}
@@ -137,35 +185,26 @@ namespace Barotrauma
}
}
private static bool PerformCoroutineStep(CoroutineHandle handle)
{
var current = handle.Coroutine.Current;
if (current != null)
{
if (current.EndsCoroutine(handle) || handle.AbortRequested) { return true; }
if (!current.CheckFinished(UnscaledDeltaTime)) { return false; }
}
if (!handle.Coroutine.MoveNext()) { return true; }
return false;
}
public static void ExecuteCoroutineThread(CoroutineHandle handle)
{
try
{
while (!handle.AbortRequested)
{
if (handle.Coroutine.Current != null)
{
WaitForSeconds wfs = handle.Coroutine.Current as WaitForSeconds;
if (wfs != null)
{
Thread.Sleep((int)(wfs.TotalTime * 1000));
}
else
{
switch ((CoroutineStatus)handle.Coroutine.Current)
{
case CoroutineStatus.Success:
return;
case CoroutineStatus.Failure:
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
return;
}
}
}
Thread.Yield();
if (!handle.Coroutine.MoveNext()) return;
if (PerformCoroutineStep(handle)) { return; }
Thread.Sleep((int)(UnscaledDeltaTime * 1000));
}
}
catch (ThreadAbortException)
@@ -187,36 +226,13 @@ namespace Barotrauma
#endif
if (handle.Thread == null)
{
if (handle.AbortRequested) { return true; }
if (handle.Coroutine.Current != null)
{
WaitForSeconds wfs = handle.Coroutine.Current as WaitForSeconds;
if (wfs != null)
{
if (!wfs.CheckFinished(UnscaledDeltaTime)) return false;
}
else
{
switch ((CoroutineStatus)handle.Coroutine.Current)
{
case CoroutineStatus.Success:
return true;
case CoroutineStatus.Failure:
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
return true;
}
}
}
handle.Coroutine.MoveNext();
return false;
return PerformCoroutineStep(handle);
}
else
{
if (handle.Thread.ThreadState.HasFlag(ThreadState.Stopped))
{
if (handle.Exception!=null || (CoroutineStatus)handle.Coroutine.Current == CoroutineStatus.Failure)
if (handle.Exception!=null || handle.Coroutine.Current == CoroutineStatus.Failure)
{
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
}
@@ -262,7 +278,7 @@ namespace Barotrauma
}
}
class WaitForSeconds
class WaitForSeconds : CoroutineStatus
{
public readonly float TotalTime;
@@ -276,7 +292,7 @@ namespace Barotrauma
this.ignorePause = ignorePause;
}
public bool CheckFinished(float deltaTime)
public override bool CheckFinished(float deltaTime)
{
#if !SERVER
if (ignorePause || !GUI.PauseMenuOpen)
@@ -288,5 +304,10 @@ namespace Barotrauma
#endif
return timer <= 0.0f;
}
public override bool EndsCoroutine(CoroutineHandle handle)
{
return false;
}
}
}
@@ -109,7 +109,7 @@ namespace Barotrauma
public static bool CheatsEnabled;
private static readonly List<ColoredText> unsavedMessages = new List<ColoredText>();
private static readonly int messagesPerFile = 5000;
private static readonly int messagesPerFile = 800;
public const string SavePath = "ConsoleLogs";
private static void AssignOnExecute(string names, Action<string[]> onExecute)
@@ -878,7 +878,7 @@ namespace Barotrauma
List<TalentTree> talentTrees = new List<TalentTree>();
if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase))
{
talentTrees.AddRange(TalentTree.JobTalentTrees.Values);
talentTrees.AddRange(TalentTree.JobTalentTrees);
}
else
{
@@ -1062,7 +1062,7 @@ namespace Barotrauma
},
null));
IEnumerable<object> TestLevels()
IEnumerable<CoroutineStatus> TestLevels()
{
SubmarineInfo selectedSub = null;
string subName = GameMain.Config.QuickStartSubmarineName;
@@ -1410,6 +1410,29 @@ namespace Barotrauma
}
}, null, isCheat: true));
commands.Add(new Command("despawnnow", "despawnnow [character]: Immediately despawns the specified dead character. If the character argument is omitted, all dead characters are despawned.", (string[] args) =>
{
if (args.Length == 0)
{
foreach (Character c in Character.CharacterList.Where(c => c.IsDead).ToList())
{
c.DespawnNow();
}
}
else
{
Character character = FindMatchingCharacter(args);
character?.DespawnNow();
}
},
() =>
{
return new string[][]
{
Character.CharacterList.Where(c => c.IsDead).Select(c => c.Name).Distinct().ToArray()
};
}, isCheat: true));
commands.Add(new Command("setclientcharacter", "setclientcharacter [client name] [character name]: Gives the client control of the specified character.", null,
() =>
{
@@ -1832,7 +1855,7 @@ namespace Barotrauma
#if CLIENT
activeQuestionText = null;
#endif
NewMessage(command, Color.White, true);
NewCommand(command);
//reset the variable before invoking the delegate because the method may need to activate another question
var temp = activeQuestionCallback;
activeQuestionCallback = null;
@@ -1857,7 +1880,7 @@ namespace Barotrauma
if (!firstCommand.Equals("admin", StringComparison.OrdinalIgnoreCase))
{
NewMessage(command, Color.White, true);
NewCommand(command);
}
#if CLIENT
@@ -2173,15 +2196,37 @@ namespace Barotrauma
}
}
public static void NewMessage(string msg, bool isCommand = false)
public static void ShowError(string msg, Color? color = null)
{
color ??= Color.Red;
NewMessage(msg, color.Value, isCommand: false, isError: true);
}
public static void NewCommand(string command, Color? color = null)
{
color ??= Color.White;
NewMessage(command, color.Value, isCommand: true, isError: false);
}
public static void NewMessage(string msg, Color? color = null, bool debugOnly = false)
{
color ??= Color.White;
if (debugOnly)
{
#if DEBUG
NewMessage(msg, color.Value, isCommand: false, isError: false);
#endif
}
else
{
NewMessage(msg, color.Value, isCommand: false, isError: false);
}
#if DEBUG
Console.WriteLine(msg);
#endif
NewMessage(msg, Color.White, isCommand);
}
public static void NewMessage(string msg, Color color, bool isCommand = false, bool isError = false)
private static void NewMessage(string msg, Color color, bool isCommand, bool isError)
{
if (string.IsNullOrEmpty(msg)) { return; }
@@ -2271,7 +2316,10 @@ namespace Barotrauma
public static void Log(string message)
{
if (GameSettings.VerboseLogging) NewMessage(message, Color.Gray);
if (GameSettings.VerboseLogging)
{
NewMessage(message, Color.Gray);
}
}
public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false)
@@ -2309,7 +2357,7 @@ namespace Barotrauma
}
#endif
NewMessage(error, Color.Red, isError: true);
ShowError(error);
}
public static void AddWarning(string warning)
@@ -2319,7 +2367,7 @@ namespace Barotrauma
}
#if CLIENT
private static IEnumerable<object> CreateMessageBox(string errorMsg)
private static IEnumerable<CoroutineStatus> CreateMessageBox(string errorMsg)
{
while (GUI.Style == null)
{
@@ -121,7 +121,7 @@ namespace Barotrauma
{
foreach (Item item in newCharacter.Inventory.AllItems)
{
item.SpawnedInOutpost = true;
item.SpawnedInCurrentOutpost = true;
item.AllowStealing = false;
}
}
@@ -33,6 +33,8 @@ namespace Barotrauma
private float currentIntensity;
//The exact intensity of the current situation, current intensity is lerped towards this value
private float targetIntensity;
//follows targetIntensity a bit faster than currentIntensity to prevent e.g. combat musing staying on very long after the monsters are dead
private float musicIntensity;
//How low the intensity has to be for an event to be triggered.
//Gradually increases with time, so additional problems can still appear eventually even if
@@ -50,7 +52,11 @@ namespace Barotrauma
private float calculateDistanceTraveledTimer;
private float distanceTraveled;
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterTotalStrength;
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterStrength;
public float CumulativeMonsterStrengthMain;
public float CumulativeMonsterStrengthRuins;
public float CumulativeMonsterStrengthWrecks;
public float CumulativeMonsterStrengthCaves;
private float roundDuration;
@@ -78,6 +84,10 @@ namespace Barotrauma
{
get { return currentIntensity; }
}
public float MusicIntensity
{
get { return musicIntensity; }
}
public List<Event> ActiveEvents
{
@@ -85,7 +95,22 @@ namespace Barotrauma
}
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
private struct TimeStamp
{
public readonly double Time;
public readonly Event Event;
public TimeStamp(Event e)
{
Event = e;
Time = Timing.TotalTime;
}
}
private readonly List<TimeStamp> timeStamps = new List<TimeStamp>();
public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e));
public EventManager()
{
isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
@@ -99,6 +124,7 @@ namespace Barotrauma
if (isClient) { return; }
timeStamps.Clear();
pendingEventSets.Clear();
selectedEvents.Clear();
activeEvents.Clear();
@@ -202,8 +228,12 @@ namespace Barotrauma
crewAwayResetTimer = 0.0f;
intensityUpdateTimer = 0.0f;
CalculateCurrentIntensity(0.0f);
currentIntensity = targetIntensity;
currentIntensity = musicIntensity = targetIntensity;
eventCoolDown = 0.0f;
CumulativeMonsterStrengthMain = 0;
CumulativeMonsterStrengthRuins = 0;
CumulativeMonsterStrengthWrecks = 0;
CumulativeMonsterStrengthCaves = 0;
}
private void SelectSettings()
@@ -401,11 +431,7 @@ namespace Barotrauma
{
if (level == null) { return; }
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
#if DEBUG
DebugConsole.NewMessage($"Loading event set {eventSet.DebugIdentifier}", Color.LightBlue);
#else
DebugConsole.Log($"Loading event set {eventSet.DebugIdentifier}");
#endif
DebugConsole.NewMessage($"Loading event set {eventSet.DebugIdentifier}", Color.LightBlue, debugOnly: true);
int applyCount = 1;
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
if (eventSet.PerRuin)
@@ -413,7 +439,7 @@ namespace Barotrauma
applyCount = level.Ruins.Count();
foreach (var ruin in level.Ruins)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Ruin == ruin; });
spawnPosFilter.Add(pos => pos.Ruin == ruin);
}
}
else if (eventSet.PerCave)
@@ -421,7 +447,7 @@ namespace Barotrauma
applyCount = level.Caves.Count();
foreach (var cave in level.Caves)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Cave == cave; });
spawnPosFilter.Add(pos => pos.Cave == cave);
}
}
else if (eventSet.PerWreck)
@@ -430,7 +456,7 @@ namespace Barotrauma
applyCount = wrecks.Count();
foreach (var wreck in wrecks)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Submarine == wreck; });
spawnPosFilter.Add(pos => pos.Submarine == wreck);
}
}
@@ -463,11 +489,7 @@ namespace Barotrauma
if (newEvent == null) { continue; }
newEvent.Init(true);
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
#if DEBUG
DebugConsole.NewMessage($"Initialized event {newEvent}");
#else
DebugConsole.Log($"Initialized event {newEvent}");
#endif
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
@@ -498,11 +520,7 @@ namespace Barotrauma
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.Init(true);
#if DEBUG
DebugConsole.NewMessage($"Initialized event {newEvent}");
#else
DebugConsole.Log($"Initialized event {newEvent}");
#endif
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
@@ -525,6 +543,7 @@ namespace Barotrauma
var allowedEventSets =
eventSets.Where(es =>
es.IsCampaignSet == GameMain.GameSession?.GameMode is CampaignMode &&
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)));
@@ -647,6 +666,7 @@ namespace Barotrauma
isCrewAway = false;
crewAwayDuration = 0.0f;
eventThreshold += settings.EventThresholdIncrease * deltaTime;
eventThreshold = Math.Min(eventThreshold, 1.0f);
eventCoolDown -= deltaTime;
}
@@ -739,7 +759,7 @@ namespace Barotrauma
// enemy amount --------------------------------------------------------
enemyDanger = 0.0f;
monsterTotalStrength = 0;
monsterStrength = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; }
@@ -749,28 +769,9 @@ namespace Barotrauma
if (!enemyAI.AIParams.StayInAbyss)
{
// Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
monsterTotalStrength += enemyAI.CombatStrength;
monsterStrength += enemyAI.CombatStrength;
}
// Example combat strengths:
// Hammerheadspawn 1
// Moloch Pupa 1
// Terminal cell 20
// Leucocyte 40
// Husk 90
// Crawler 100
// Unarmored Mudraptor 140
// Spineling 150
// Tigerthresher 200
// Armored Mudraptor 210
// Watcher 400
// Golden Hammerhead 400
// Hammerhead 500
// Hammerhead Matriarch 550
// Bonethresher 600
// Moloch 1250
// Black Moloch 1500
// Endworm 10000
if (character.CurrentHull?.Submarine != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
{
@@ -792,7 +793,7 @@ namespace Barotrauma
// 5 Mudraptors -> +0.21 (0.42 in total, before they get inside).
// 3 Hammerheads -> +0.3 (0.6 in total, if they all target the sub).
// 2 Molochs -> +0.5 (1.0 in total, if both target the sub).
enemyDanger += monsterTotalStrength / 5000f;
enemyDanger += monsterStrength / 5000f;
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
// The definitions above aim for that we never spawn more monsters that the player (and the performance) can handle.
@@ -868,11 +869,15 @@ namespace Barotrauma
{
//25 seconds for intensity to go from 0.0 to 1.0
currentIntensity = Math.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity);
//20 seconds for intensity to go from 0.0 to 1.0
musicIntensity = Math.Min(musicIntensity + 0.05f * IntensityUpdateInterval, targetIntensity);
}
else
{
//400 seconds for intensity to go from 1.0 to 0.0
currentIntensity = Math.Max(currentIntensity - 0.0025f * IntensityUpdateInterval, targetIntensity);
//20 seconds for intensity to go from 1.0 to 0.0
musicIntensity = Math.Max(musicIntensity - 0.05f * IntensityUpdateInterval, targetIntensity);
}
}
@@ -13,6 +13,7 @@ namespace Barotrauma
public float Commonness;
public string Identifier;
public string BiomeIdentifier;
public float SpawnDistance;
public bool UnlockPathEvent;
public string UnlockPathTooltip;
@@ -46,25 +47,30 @@ namespace Barotrauma
UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip");
UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0);
UnlockPathFaction = element.GetAttributeString("unlockpathfaction", "");
SpawnDistance = element.GetAttributeFloat("spawndistance", 0);
}
public bool TryCreateInstance<T>(out T instance) where T : Event
{
instance = CreateInstance() as T;
return instance is T;
}
public Event CreateInstance()
{
ConstructorInfo constructor = EventType.GetConstructor(new[] { typeof(EventPrefab) });
object instance = null;
Event instance = null;
try
{
instance = constructor.Invoke(new object[] { this });
instance = constructor.Invoke(new object[] { this }) as Event;
}
catch (Exception ex)
{
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
}
Event ev = (Event)instance;
if (!ev.LevelMeetsRequirements()) { return null; }
return (Event)instance;
if (instance != null && !instance.LevelMeetsRequirements()) { return null; }
return instance;
}
public override string ToString()
@@ -14,6 +14,7 @@ namespace Barotrauma
{
public readonly EventSet RootSet;
public readonly Dictionary<string, int> MonsterCounts = new Dictionary<string, int>();
public float MonsterStrength;
public EventDebugStats(EventSet rootSet)
{
@@ -63,6 +64,8 @@ namespace Barotrauma
return GetAllEventPrefabs().Find(prefab => string.Equals(prefab.Identifier, identifer, StringComparison.Ordinal));
}
public readonly bool IsCampaignSet;
//0-100
public readonly float MinLevelDifficulty, MaxLevelDifficulty;
@@ -193,6 +196,7 @@ namespace Barotrauma
DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck);
OncePerOutpost = element.GetAttributeBool("onceperoutpost", false);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost);
Commonness[""] = element.GetAttributeFloat("commonness", 1.0f);
foreach (XElement subElement in element.Elements())
@@ -205,7 +209,7 @@ namespace Barotrauma
{
if (overrideElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase))
{
string levelType = overrideElement.GetAttributeString("leveltype", "");
string levelType = overrideElement.GetAttributeString("leveltype", "").ToLowerInvariant();
if (!Commonness.ContainsKey(levelType))
{
Commonness.Add(levelType, overrideElement.GetAttributeFloat("commonness", 0.0f));
@@ -227,8 +231,8 @@ namespace Barotrauma
EventPrefabs.Add(new SubEventPrefab(
debugIdentifier,
identifiers,
commonness>=0f ? commonness : (float?)null,
probability>=0f ? probability : (float?)null));
commonness >= 0f ? commonness : (float?)null,
probability >= 0f ? probability : (float?)null));
}
else
{
@@ -347,7 +351,7 @@ namespace Barotrauma
}
}
public static List<string> GetDebugStatistics(int simulatedRoundCount = 100)
public static List<string> GetDebugStatistics(int simulatedRoundCount = 100, Func<MonsterEvent, bool> filter = null)
{
List<string> debugLines = new List<string>();
@@ -357,82 +361,75 @@ namespace Barotrauma
for (int i = 0; i < simulatedRoundCount; i++)
{
var newStats = new EventDebugStats(eventSet);
CheckEventSet(newStats, eventSet);
CheckEventSet(newStats, eventSet, filter);
stats.Add(newStats);
}
debugLines.Add($"Event stats ({eventSet.DebugIdentifier}): ");
LogEventStats(stats, debugLines);
}
for (int difficulty = 0; difficulty <= 100; difficulty += 10)
{
debugLines.Add($"Event stats on difficulty level {difficulty}: ");
List<EventDebugStats> stats = new List<EventDebugStats>();
for (int i = 0; i < simulatedRoundCount; i++)
{
EventSet selectedSet = List.Where(s => difficulty >= s.MinLevelDifficulty && difficulty <= s.MaxLevelDifficulty).GetRandom();
if (selectedSet == null) { continue; }
var newStats = new EventDebugStats(selectedSet);
CheckEventSet(newStats, selectedSet);
stats.Add(newStats);
}
LogEventStats(stats, debugLines);
}
return debugLines;
static void CheckEventSet(EventDebugStats stats, EventSet thisSet)
static void CheckEventSet(EventDebugStats stats, EventSet thisSet, Func<MonsterEvent, bool> filter = null)
{
if (thisSet.ChooseRandom)
{
var unusedEvents = thisSet.EventPrefabs.ToList();
for (int i = 0; i < thisSet.EventCount; i++)
if (unusedEvents.Any())
{
var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList(), Rand.RandSync.Unsynced);
if (eventPrefab.Prefabs.Any(p => p != null))
for (int i = 0; i < thisSet.EventCount; i++)
{
AddEvents(stats, eventPrefab.Prefabs);
unusedEvents.Remove(eventPrefab);
var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList());
if (eventPrefab.Prefabs.Any(p => p != null))
{
AddEvents(stats, eventPrefab.Prefabs, filter);
unusedEvents.Remove(eventPrefab);
}
}
}
List<float> values = thisSet.ChildSets.SelectMany(s => s.Commonness.Values).ToList();
EventSet childSet = ToolBox.SelectWeightedRandom(thisSet.ChildSets, values);
if (childSet != null)
{
CheckEventSet(stats, childSet, filter);
}
}
else
{
foreach (var eventPrefab in thisSet.EventPrefabs)
{
AddEvents(stats, eventPrefab.Prefabs);
AddEvents(stats, eventPrefab.Prefabs, filter);
}
foreach (var childSet in thisSet.ChildSets)
{
CheckEventSet(stats, childSet, filter);
}
}
foreach (var childSet in thisSet.ChildSets)
{
CheckEventSet(stats, childSet);
}
}
static void AddEvents(EventDebugStats stats, IEnumerable<EventPrefab> eventPrefabs)
=> eventPrefabs.ForEach(p => AddEvent(stats, p));
static void AddEvents(EventDebugStats stats, IEnumerable<EventPrefab> eventPrefabs, Func<MonsterEvent, bool> filter = null)
=> eventPrefabs.ForEach(p => AddEvent(stats, p, filter));
static void AddEvent(EventDebugStats stats, EventPrefab eventPrefab)
static void AddEvent(EventDebugStats stats, EventPrefab eventPrefab, Func<MonsterEvent, bool> filter = null)
{
if (eventPrefab.EventType == typeof(MonsterEvent))
if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent))
{
float spawnProbability = eventPrefab.ConfigElement.GetAttributeFloat("spawnprobability", 1.0f);
if (Rand.Value(Rand.RandSync.Server) > spawnProbability)
{
return;
}
if (filter != null && !filter(monsterEvent)) { return; }
string character = eventPrefab.ConfigElement.GetAttributeString("characterfile", "");
System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(character));
int amount = eventPrefab.ConfigElement.GetAttributeInt("amount", 0);
int minAmount = eventPrefab.ConfigElement.GetAttributeInt("minamount", amount);
int maxAmount = eventPrefab.ConfigElement.GetAttributeInt("maxamount", amount);
float spawnProbability = monsterEvent.Prefab.Probability;
if (Rand.Value() > spawnProbability) { return; }
int count = Rand.Range(minAmount, maxAmount + 1);
string character = monsterEvent.speciesName;
int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1);
if (count <= 0) { return; }
if (!stats.MonsterCounts.ContainsKey(character)) { stats.MonsterCounts[character] = 0; }
stats.MonsterCounts[character] += count;
var aiElement = CharacterPrefab.FindBySpeciesName(character)?.XDocument?.Root?.GetChildElement("ai");
if (aiElement != null)
{
stats.MonsterStrength += aiElement.GetAttributeFloat("combatstrength", 0) * count;
}
}
}
@@ -445,16 +442,21 @@ namespace Barotrauma
}
else
{
stats.Sort((s1, s2) => { return s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum()); });
EventDebugStats minStats = stats.First();
EventDebugStats maxStats = stats.First();
debugLines.Add($" Minimum monster spawns: {stats.First().MonsterCounts.Values.Sum()}");
stats.Sort((s1, s2) => s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum()));
debugLines.Add($" Minimum monster count: {stats.First().MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats.First())}");
debugLines.Add($" Median monster spawns: {stats[stats.Count / 2].MonsterCounts.Values.Sum()}");
debugLines.Add($" Median monster count: {stats[stats.Count / 2].MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats[stats.Count / 2])}");
debugLines.Add($" Maximum monster spawns: {stats.Last().MonsterCounts.Values.Sum()}");
debugLines.Add($" Maximum monster count: {stats.Last().MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats.Last())}");
debugLines.Add($" Average monster count: {StringFormatter.FormatZeroDecimal((float)stats.Average(s => s.MonsterCounts.Values.Sum()))}");
debugLines.Add($" ");
stats.Sort((s1, s2) => s1.MonsterStrength.CompareTo(s2.MonsterStrength));
debugLines.Add($" Minimum monster strength: {StringFormatter.FormatZeroDecimal(stats.First().MonsterStrength)}");
debugLines.Add($" Median monster strength: {StringFormatter.FormatZeroDecimal(stats[stats.Count / 2].MonsterStrength)}");
debugLines.Add($" Maximum monster strength: {StringFormatter.FormatZeroDecimal(stats.Last().MonsterStrength)}");
debugLines.Add($" Average monster strength: {StringFormatter.FormatZeroDecimal(stats.Average(s => s.MonsterStrength))}");
debugLines.Add($" ");
}
}
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -161,6 +162,24 @@ namespace Barotrauma
#endif
}
}
//if any of the target items is a reactor, prevent exploding it from damaging the player's sub
foreach (Item item in items)
{
if (item.GetComponent<Reactor>() is Reactor reactor && (reactor.statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false))
{
foreach (var statusEffect in reactor.statusEffectLists[ActionType.OnBroken])
{
foreach (Explosion explosion in statusEffect.Explosions)
{
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.TeamID == CharacterTeamType.Team1) { explosion.IgnoredSubmarines.Add(sub); }
}
}
}
}
}
}
private void InitCharacters(Submarine submarine)
@@ -207,7 +207,7 @@ namespace Barotrauma
var item = new Item(itemPrefab, position.Value, cargoRoomSub)
{
SpawnedInOutpost = true,
SpawnedInCurrentOutpost = true,
AllowStealing = false
};
item.FindHull();
@@ -1,4 +1,3 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
@@ -6,8 +5,6 @@ namespace Barotrauma
partial class CombatMission : Mission
{
private Submarine[] subs;
// TODO: not used
private List<Character>[] crews;
private readonly string[] descriptions;
private static string[] teamNames = { "Team A", "Team B" };
@@ -103,15 +100,16 @@ namespace Barotrauma
subs[0].NeutralizeBallast();
subs[0].TeamID = CharacterTeamType.Team1;
subs[0].DockedTo.ForEach(s => s.TeamID = CharacterTeamType.Team1);
subs[0].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team1);
subs[1].NeutralizeBallast();
subs[1].TeamID = CharacterTeamType.Team2;
subs[1].DockedTo.ForEach(s => s.TeamID = CharacterTeamType.Team2);
subs[1].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team2);
subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition));
subs[1].FlipX();
#if SERVER
crews = new List<Character>[] { new List<Character>(), new List<Character>() };
#endif
}
public override void End()
@@ -203,6 +203,14 @@ namespace Barotrauma
enemySub.TeamID = CharacterTeamType.None;
//make the enemy sub withstand atleast the same depth as the player sub
enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth);
if (Level.Loaded != null)
{
//...and the depth of the patrol positions + 1000 m
foreach (var patrolPos in patrolPositions)
{
enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Level.Loaded.GetRealWorldDepth(patrolPos.Y) + 1000);
}
}
enemySub.ImmuneToBallastFlora = true;
}
@@ -9,8 +9,8 @@ namespace Barotrauma
{
class MonsterEvent : Event
{
private readonly string speciesName;
private readonly int minAmount, maxAmount;
public readonly string speciesName;
public readonly int minAmount, maxAmount;
private List<Character> monsters;
private readonly float scatter;
@@ -20,7 +20,7 @@ namespace Barotrauma
private bool disallowed;
private readonly Level.PositionType spawnPosType;
public readonly Level.PositionType SpawnPosType;
private readonly string spawnPointTag;
private bool spawnPending;
@@ -77,15 +77,15 @@ namespace Barotrauma
var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPosTypeStr) ||
!Enum.TryParse(spawnPosTypeStr, true, out spawnPosType))
!Enum.TryParse(spawnPosTypeStr, true, out SpawnPosType))
{
spawnPosType = Level.PositionType.MainPath;
SpawnPosType = Level.PositionType.MainPath;
}
//backwards compatibility
if (prefab.ConfigElement.GetAttributeBool("spawndeep", false))
{
spawnPosType = Level.PositionType.Abyss;
SpawnPosType = Level.PositionType.Abyss;
}
spawnPointTag = prefab.ConfigElement.GetAttributeString("spawnpointtag", string.Empty);
@@ -143,7 +143,7 @@ namespace Barotrauma
private List<Level.InterestingPosition> GetAvailableSpawnPositions()
{
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType));
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => SpawnPosType.HasFlag(p.PositionType));
var removals = new List<Level.InterestingPosition>();
foreach (var position in availablePositions)
{
@@ -188,8 +188,8 @@ namespace Barotrauma
spawnPos = Vector2.Zero;
var availablePositions = GetAvailableSpawnPositions();
var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false);
bool isRuinOrWreck = spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Wreck);
if (affectSubImmediately && !isRuinOrWreck && !spawnPosType.HasFlag(Level.PositionType.Abyss))
bool isRuinOrWreck = SpawnPosType.HasFlag(Level.PositionType.Ruin) || SpawnPosType.HasFlag(Level.PositionType.Wreck);
if (affectSubImmediately && !isRuinOrWreck && !SpawnPosType.HasFlag(Level.PositionType.Abyss))
{
if (availablePositions.None())
{
@@ -288,11 +288,14 @@ namespace Barotrauma
spawnPos = chosenPosition.Position.ToVector2();
if (chosenPosition.Submarine != null || chosenPosition.Ruin != null)
{
var spawnPoint =
WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag);
bool ignoreSubmarine = chosenPosition.Ruin != null;
var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag, ignoreSubmarine: ignoreSubmarine);
if (spawnPoint != null)
{
System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine));
if (!ignoreSubmarine)
{
System.Diagnostics.Debug.Assert(spawnPoint.Submarine == chosenPosition.Submarine);
}
spawnPos = spawnPoint.WorldPosition;
}
else
@@ -303,32 +306,42 @@ namespace Barotrauma
return;
}
}
else if ((chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath)
&& offset > 0)
else if (chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath)
{
Vector2 dir;
var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null);
var nearestWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value)).FirstOrDefault();
if (nearestWaypoint != null)
if (offset > 0)
{
int currentIndex = waypoints.IndexOf(nearestWaypoint);
var nextWaypoint = waypoints[Math.Min(currentIndex + 20, waypoints.Count - 1)];
dir = Vector2.Normalize(nextWaypoint.WorldPosition - nearestWaypoint.WorldPosition);
// Ensure that the spawn position is not offset to the left.
if (dir.X < 0)
Vector2 dir;
var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null && wp.Ruin == null);
var nearestWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value)).FirstOrDefault();
if (nearestWaypoint != null)
{
dir.X = 0;
int currentIndex = waypoints.IndexOf(nearestWaypoint);
var nextWaypoint = waypoints[Math.Min(currentIndex + 20, waypoints.Count - 1)];
dir = Vector2.Normalize(nextWaypoint.WorldPosition - nearestWaypoint.WorldPosition);
// Ensure that the spawn position is not offset to the left.
if (dir.X < 0)
{
dir.X = 0;
}
}
else
{
dir = new Vector2(1, Rand.Range(-1, 1));
}
Vector2 targetPos = spawnPos.Value + dir * offset;
var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault();
if (targetWaypoint != null)
{
spawnPos = targetWaypoint.WorldPosition;
}
}
else
// Ensure that the position is not inside a submarine (in practice wrecks).
if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(spawnPos.Value)))
{
dir = new Vector2(1, Rand.Range(-1, 1));
}
Vector2 targetPos = spawnPos.Value + dir * offset;
var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault();
if (targetWaypoint != null)
{
spawnPos = targetWaypoint.WorldPosition;
//no suitable position found, disable the event
spawnPos = null;
Finished();
return;
}
}
spawnPending = true;
@@ -371,7 +384,7 @@ namespace Barotrauma
if (spawnPending)
{
//wait until there are no submarines at the spawnpos
if (spawnPosType.HasFlag(Level.PositionType.MainPath) || spawnPosType.HasFlag(Level.PositionType.SidePath) || spawnPosType.HasFlag(Level.PositionType.Abyss))
if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath) || SpawnPosType.HasFlag(Level.PositionType.Abyss))
{
foreach (Submarine submarine in Submarine.Loaded)
{
@@ -380,17 +393,29 @@ namespace Barotrauma
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; }
}
}
//if spawning in a ruin/cave, wait for someone to be close to it to spawning
//unnecessary monsters in places the players might never visit during the round
if (spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Cave) || spawnPosType.HasFlag(Level.PositionType.Wreck))
float minDistance = Prefab.SpawnDistance;
if (minDistance <= 0)
{
if (SpawnPosType.HasFlag(Level.PositionType.Cave))
{
minDistance = 8000;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Ruin))
{
minDistance = 5000;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Wreck))
{
minDistance = 3000;
}
}
if (minDistance > 0)
{
bool someoneNearby = false;
float minDist = Sonar.DefaultSonarRange * 0.8f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist)
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance))
{
someoneNearby = true;
break;
@@ -400,7 +425,7 @@ namespace Barotrauma
{
if (c == Character.Controlled || c.IsRemotePlayer)
{
if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < minDist * minDist)
if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance))
{
someoneNearby = true;
break;
@@ -411,7 +436,7 @@ namespace Barotrauma
}
if (spawnPosType.HasFlag(Level.PositionType.Abyss) || spawnPosType.HasFlag(Level.PositionType.AbyssCave))
if (SpawnPosType.HasFlag(Level.PositionType.Abyss) || SpawnPosType.HasFlag(Level.PositionType.AbyssCave))
{
bool anyInAbyss = false;
foreach (Submarine submarine in Submarine.Loaded)
@@ -432,7 +457,7 @@ namespace Barotrauma
int amount = Rand.Range(minAmount, maxAmount + 1);
monsters = new List<Character>();
float scatterAmount = scatter;
if (spawnPosType.HasFlag(Level.PositionType.SidePath))
if (SpawnPosType.HasFlag(Level.PositionType.SidePath))
{
var sidePaths = Level.Loaded.Tunnels.Where(t => t.Type == Level.TunnelType.SidePath);
if (sidePaths.Any())
@@ -444,7 +469,7 @@ namespace Barotrauma
scatterAmount = scatter;
}
}
else if (!spawnPosType.HasFlag(Level.PositionType.MainPath))
else if (!SpawnPosType.HasFlag(Level.PositionType.MainPath))
{
scatterAmount = 0;
}
@@ -474,6 +499,27 @@ namespace Barotrauma
}
Character createdCharacter = Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true);
var eventManager = GameMain.GameSession.EventManager;
if (eventManager != null)
{
if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath))
{
eventManager.CumulativeMonsterStrengthMain += createdCharacter.Params.AI.CombatStrength;
eventManager.AddTimeStamp(this);
}
else if (SpawnPosType.HasFlag(Level.PositionType.Ruin))
{
eventManager.CumulativeMonsterStrengthRuins += createdCharacter.Params.AI.CombatStrength;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Wreck))
{
eventManager.CumulativeMonsterStrengthWrecks += createdCharacter.Params.AI.CombatStrength;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Cave))
{
eventManager.CumulativeMonsterStrengthCaves += createdCharacter.Params.AI.CombatStrength;
}
}
if (GameMain.GameSession.IsCurrentLocationRadiated())
{
AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness;
@@ -490,6 +536,7 @@ namespace Barotrauma
//this will do nothing if the monsters have no swarm behavior defined,
//otherwise it'll make the spawned characters act as a swarm
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true);
}
}, Rand.Range(0f, amount / 2f));
}
@@ -56,7 +56,7 @@ namespace Barotrauma
public static string Format(this float value, int decimalCount)
{
return value.ToString($"F{decimalCount.ToString()}", CultureInfo.InvariantCulture);
return value.ToString($"F{decimalCount}", CultureInfo.InvariantCulture);
}
public static string FormatSingleDecimal(this Vector2 value)
@@ -254,7 +254,7 @@ namespace Barotrauma
if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; }
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine)
{
SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost,
SpawnedInCurrentOutpost = validContainer.Key.Item.SpawnedInCurrentOutpost,
AllowStealing = validContainer.Key.Item.AllowStealing,
Quality = quality,
OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex,
@@ -393,7 +393,7 @@ namespace Barotrauma
/// </summary>
protected abstract void LoadInitialLevel();
protected abstract IEnumerable<object> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List<TraitorMissionResult> traitorResults = null);
protected abstract IEnumerable<CoroutineStatus> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List<TraitorMissionResult> traitorResults = null);
/// <summary>
/// Which type of transition between levels is currently possible (if any)
@@ -572,7 +572,7 @@ namespace Barotrauma
{
foreach (Item item in Item.ItemList)
{
if (!item.SpawnedInOutpost || item.OriginalModuleIndex < 0) { continue; }
if (!item.SpawnedInCurrentOutpost || item.OriginalModuleIndex < 0) { continue; }
var owner = item.GetRootInventoryOwner();
if ((!(owner?.Submarine?.Info?.IsOutpost ?? false)) || (owner is Character character && character.TeamID == CharacterTeamType.Team1) || item.Submarine == null || !item.Submarine.Info.IsOutpost)
{
@@ -712,7 +712,7 @@ namespace Barotrauma
}
}
private IEnumerable<object> DoCharacterWait(Character npc, Character interactor)
private IEnumerable<CoroutineStatus> DoCharacterWait(Character npc, Character interactor)
{
if (npc == null || interactor == null) { yield return CoroutineStatus.Failure; }
@@ -907,7 +907,7 @@ namespace Barotrauma
public int NumberOfMissionsAtLocation(Location location)
{
return Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location));
return Map?.CurrentLocation?.SelectedMissions?.Count(m => m.Locations.Contains(location)) ?? 0;
}
public void CheckTooManyMissions(Location currentLocation, Client sender)
@@ -312,7 +312,7 @@ namespace Barotrauma
return isRadiated;
}
public void StartRound(string levelSeed, float? difficulty = null)
public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams levelGenerationParams = null)
{
LevelData randomLevel = null;
foreach (Mission mission in Missions.Union(GameMode.Missions))
@@ -324,11 +324,11 @@ namespace Barotrauma
{
LocationType locationType = LocationType.List.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m.Equals(lt.Identifier, StringComparison.OrdinalIgnoreCase)));
CreateDummyLocations(locationType);
randomLevel = LevelData.CreateRandom(levelSeed, difficulty, requireOutpost: true);
randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true);
break;
}
}
randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty);
randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams);
StartRound(randomLevel);
}
@@ -351,6 +351,8 @@ namespace Barotrauma
return;
}
Submarine.LockX = Submarine.LockY = false;
LevelData = levelData;
Submarine.Unload();
@@ -511,10 +513,6 @@ namespace Barotrauma
mpCampaign.UpgradeManager.ApplyUpgrades();
mpCampaign.UpgradeManager.SanityCheckUpgrades(Submarine);
}
if (GameMode is CampaignMode)
{
Submarine.WarmStartPower();
}
}
GameMain.Config.RecentlyEncounteredCreatures.Clear();
@@ -158,7 +158,7 @@ namespace Barotrauma.Items.Components
if (!CanBeCombinedWith(otherGeneticMaterial)) { return false; }
float conditionIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax);
conditionIncrease += user.GetStatValue(StatTypes.GeneticMaterialRefineBonus);
conditionIncrease += user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f;
if (item.Prefab == otherGeneticMaterial.item.Prefab)
{
item.Condition = Math.Max(item.Condition, otherGeneticMaterial.item.Condition) + conditionIncrease;
@@ -110,7 +110,9 @@ namespace Barotrauma.Items.Components
if (Item.RequireAimToUse && hitPos < MathHelper.PiOver4) { return false; }
ActivateNearbySleepingCharacters();
reloadTimer = reload / (1 + character.GetStatValue(StatTypes.MeleeAttackSpeed));
reloadTimer = reload;
reloadTimer /= (1f + character.GetStatValue(StatTypes.MeleeAttackSpeed));
reloadTimer /= (1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier));
item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile;
item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall;
@@ -385,7 +387,7 @@ namespace Barotrauma.Items.Components
{
Attack.SetUser(User);
Attack.DamageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier);
Attack.DamageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.AttackMultiplier);
Attack.DamageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.StrikingPowerMultiplier);
if (targetLimb != null)
{
@@ -142,7 +142,7 @@ namespace Barotrauma.Items.Components
return false;
}
private IEnumerable<object> WaitForPick(Character picker, float requiredTime)
private IEnumerable<CoroutineStatus> WaitForPick(Character picker, float requiredTime)
{
activePicker = picker;
picker.PickingItem = item;
@@ -149,7 +149,8 @@ namespace Barotrauma.Items.Components
{
float degreeOfFailure = 1.0f - DegreeOfSuccess(user);
degreeOfFailure *= degreeOfFailure;
return MathHelper.ToRadians(MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure));
float spread = MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure) / (1f + user.GetStatValue(StatTypes.RangedSpreadReduction));
return MathHelper.ToRadians(spread);
}
private readonly List<Body> limbBodies = new List<Body>();
@@ -203,7 +204,7 @@ namespace Barotrauma.Items.Components
{
lastProjectile?.Item.GetComponent<Rope>()?.Snap();
}
float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.AttackMultiplier);
float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.StoppingPowerMultiplier);
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false, damageMultiplier);
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
if (i == 0)
@@ -632,6 +632,10 @@ namespace Barotrauma.Items.Components
public virtual void FlipY(bool relativeToSub) { }
public bool IsLoaded(Character user, bool checkContainedItems = true) =>
HasRequiredContainedItems(user, addMessage: false) &&
(!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
public bool HasRequiredContainedItems(Character user, bool addMessage, string msg = null)
{
if (!requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; }
@@ -470,7 +470,9 @@ namespace Barotrauma.Items.Components
if (!AllowDragAndDrop && user != null) { return false; }
if (!slotRestrictions.Any(s => s.MatchesItem(item))) { return false; }
if (user != null && !user.CanAccessInventory(Inventory)) { return false; }
//genetic materials use special logic for combining, don't allow doing it by placing them inside each other here
if (item.GetComponent<GeneticMaterial>() != null) { return false; }
if (Inventory.TryPutItem(item, user))
{
IsActive = true;
@@ -13,6 +13,8 @@ namespace Barotrauma.Items.Components
partial void OnStateChanged();
private string prevColorSignal;
public override void ReceiveSignal(Signal signal, Connection connection)
{
switch (connection.Name)
@@ -22,6 +24,13 @@ namespace Barotrauma.Items.Components
Text = signal.value;
OnStateChanged();
break;
case "set_text_color":
if (signal.value != prevColorSignal)
{
TextColor = XMLExtensions.ParseColor(signal.value, false);
prevColorSignal = signal.value;
}
break;
}
}
}
@@ -221,7 +221,7 @@ namespace Barotrauma.Items.Components
if (targetItem == otherItem) { continue; }
if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r.Equals(otherItem.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)))
{
user.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined);
user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined);
foreach (Character character in Character.GetFriendlyCrew(user))
{
character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined);
@@ -264,6 +264,8 @@ namespace Barotrauma.Items.Components
{
Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) =>
{
spawnedItem.StolenDuringRound = targetItem.StolenDuringRound;
spawnedItem.AllowStealing = targetItem.AllowStealing;
for (int i = 0; i < outputContainer.Capacity; i++)
{
var containedItem = outputContainer.Inventory.GetItemAt(i);
@@ -283,7 +285,13 @@ namespace Barotrauma.Items.Components
foreach (ItemContainer ic in targetItem.GetComponents<ItemContainer>())
{
if (ic?.Inventory == null || ic.RemoveContainedItemsOnDeconstruct) { continue; }
ic.Inventory.AllItemsMod.ForEach(containedItem => outputContainer.Inventory.TryPutItem(containedItem, user: null));
foreach (Item containedItem in ic.Inventory.AllItemsMod)
{
if (!outputContainer.Inventory.TryPutItem(containedItem, user: null))
{
containedItem.Drop(dropper: null);
}
}
}
inputContainer.Inventory.RemoveItem(targetItem);
Entity.Spawner.AddToRemoveQueue(targetItem);
@@ -20,6 +20,11 @@ namespace Barotrauma.Items.Components
private string savedFabricatedItem;
private float savedTimeUntilReady, savedRequiredTime;
private readonly Dictionary<string, List<Item>> availableIngredients = new Dictionary<string, List<Item>>();
const float RefreshIngredientsInterval = 1.0f;
private float refreshIngredientsTimer;
private bool hasPower;
private Character user;
@@ -174,6 +179,8 @@ namespace Barotrauma.Items.Components
if (selectedItem == null) { return; }
if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
RefreshAvailableIngredients();
#if CLIENT
itemList.Enabled = false;
activateButton.Text = TextManager.Get("FabricatorCancel");
@@ -242,7 +249,13 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
var availableIngredients = GetAvailableIngredients();
if (refreshIngredientsTimer <= 0.0f)
{
RefreshAvailableIngredients();
refreshIngredientsTimer = RefreshIngredientsInterval;
}
refreshIngredientsTimer -= deltaTime;
if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user))
{
CancelFabricating();
@@ -271,56 +284,87 @@ namespace Barotrauma.Items.Components
State = FabricatorState.Active;
}
float tinkeringStrength = 0f;
var repairable = item.GetComponent<Repairable>();
if (repairable != null)
{
repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f;
if (repairable.IsTinkering)
{
tinkeringStrength = repairable.TinkeringStrength;
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
if (powerConsumption <= 0) { Voltage = 1.0f; }
float tinkeringStrength = 0f;
if (repairable.IsTinkering)
{
tinkeringStrength = repairable.TinkeringStrength;
}
float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease;
timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(Voltage, 1.0f);
UpdateRequiredTimeProjSpecific();
if (timeUntilReady > 0.0f) { return; }
if (timeUntilReady <= 0.0f)
{
Fabricate();
}
}
private void Fabricate()
{
RefreshAvailableIngredients();
if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user))
{
CancelFabricating();
return;
}
bool ingredientsStolen = false;
bool ingredientsAllowStealing = true;
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
fabricatedItem.RequiredItems.ForEach(requiredItem => {
fabricatedItem.RequiredItems.ForEach(requiredItem =>
{
for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++)
{
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
{
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
var availablePrefabs = availableIngredients[requiredPrefab.Identifier];
var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab =>
var availableItems = availableIngredients[requiredPrefab.Identifier];
var availableItem = availableItems.FirstOrDefault(potentialPrefab =>
{
return potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f &&
potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f;
});
if (availablePrefab == null) { continue; }
if (availableItem == null) { continue; }
if (requiredItem.UseCondition && availablePrefab.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0
ingredientsStolen |= availableItem.StolenDuringRound;
if (!availableItem.AllowStealing)
{
availablePrefab.Condition -= availablePrefab.Prefab.Health * requiredItem.MinCondition;
continue;
ingredientsAllowStealing = false;
}
availablePrefabs.Remove(availablePrefab);
Entity.Spawner.AddToRemoveQueue(availablePrefab);
inputContainer.Inventory.RemoveItem(availablePrefab);
//Leave it behind with reduced condition if it has enough to stay above 0
if (requiredItem.UseCondition && availableItem.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f)
{
availableItem.Condition -= availableItem.Prefab.Health * requiredItem.MinCondition;
continue;
}
if (availableItem.OwnInventory != null)
{
foreach (Item containedItem in availableItem.OwnInventory.AllItemsMod)
{
containedItem.Drop(dropper: null);
}
}
availableItems.Remove(availableItem);
Entity.Spawner.AddToRemoveQueue(availableItem);
inputContainer.Inventory.RemoveItem(availableItem);
}
}
});
@@ -351,6 +395,9 @@ namespace Barotrauma.Items.Components
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
spawnedItem.Quality = quality;
spawnedItem.StolenDuringRound = ingredientsStolen;
spawnedItem.AllowStealing = ingredientsAllowStealing;
//reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers
spawnedItem.Condition = spawnedItem.MaxCondition * outCondition;
});
@@ -361,6 +408,9 @@ namespace Barotrauma.Items.Components
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
spawnedItem.Quality = quality;
spawnedItem.StolenDuringRound = ingredientsStolen;
spawnedItem.AllowStealing = ingredientsAllowStealing;
//reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers
spawnedItem.Condition = spawnedItem.MaxCondition * outCondition;
});
@@ -408,6 +458,7 @@ namespace Barotrauma.Items.Components
CancelFabricating();
}
}
private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
@@ -446,8 +497,8 @@ namespace Barotrauma.Items.Components
var availablePrefabs = availableIngredients[requiredPrefab.Identifier];
foreach (Item availablePrefab in availablePrefabs)
{
if (availablePrefab.Condition / availablePrefab.Prefab.Health >= requiredItem.MinCondition &&
availablePrefab.Condition / availablePrefab.Prefab.Health <= requiredItem.MaxCondition)
if (availablePrefab.ConditionPercentage / 100.0f >= requiredItem.MinCondition &&
availablePrefab.ConditionPercentage / 100.0f <= requiredItem.MaxCondition)
{
availablePrefabsAmount++;
}
@@ -490,14 +541,10 @@ namespace Barotrauma.Items.Components
return SkillRequirementMultiplier;
}
/// <summary>
/// Get a list of all items available in the input container and linked containers
/// </summary>
/// <returns></returns>
private Dictionary<string, List<Item>> GetAvailableIngredients()
private void RefreshAvailableIngredients()
{
List<Item> availableIngredients = new List<Item>();
availableIngredients.AddRange(inputContainer.Inventory.AllItems);
List<Item> itemList = new List<Item>();
itemList.AddRange(inputContainer.Inventory.AllItems);
foreach (MapEntity linkedTo in item.linkedTo)
{
if (linkedTo is Item linkedItem)
@@ -511,34 +558,38 @@ namespace Barotrauma.Items.Components
itemContainer = deconstructor.OutputContainer;
}
availableIngredients.AddRange(itemContainer.Inventory.AllItems);
itemList.AddRange(itemContainer.Inventory.AllItems);
}
}
for (int i = 0; i < itemList.Count; i++)
{
var container = itemList[i].GetComponent<ItemContainer>();
if (container != null)
{
itemList.AddRange(container.Inventory.AllItems);
}
}
#if CLIENT
if (Character.Controlled?.Inventory != null)
{
availableIngredients.AddRange(Character.Controlled.Inventory.AllItems);
itemList.AddRange(Character.Controlled.Inventory.AllItems);
}
#else
if (user?.Inventory != null)
{
availableIngredients.AddRange(user.Inventory.AllItems);
itemList.AddRange(user.Inventory.AllItems);
}
#endif
Dictionary<string, List<Item>> ingredientsDictionary = new Dictionary<string, List<Item>>();
for (int i = 0; i < availableIngredients.Count; i++)
availableIngredients.Clear();
foreach (Item item in itemList)
{
var itemIdentifier = availableIngredients[i].prefab.Identifier;
if (!ingredientsDictionary.ContainsKey(itemIdentifier))
var itemIdentifier = item.prefab.Identifier;
if (!availableIngredients.ContainsKey(itemIdentifier))
{
ingredientsDictionary[itemIdentifier] = new List<Item>(availableIngredients.Count);
availableIngredients[itemIdentifier] = new List<Item>(itemList.Count);
}
ingredientsDictionary[itemIdentifier].Add(availableIngredients[i]);
availableIngredients[itemIdentifier].Add(item);
}
return ingredientsDictionary;
}
/// <summary>
@@ -552,7 +603,6 @@ namespace Barotrauma.Items.Components
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
var availableIngredients = GetAvailableIngredients();
targetItem.RequiredItems.ForEach(requiredItem => {
for (int i = 0; i < requiredItem.Amount; i++)
{
@@ -588,6 +638,7 @@ namespace Barotrauma.Items.Components
}
}
});
RefreshAvailableIngredients();
}
public override XElement Save(XElement parentElement)
@@ -190,28 +190,38 @@ namespace Barotrauma.Items.Components
#if CLIENT
if (GameMain.Client != null) { return false; }
#endif
if (objective.Option.Equals("stoppumping", StringComparison.OrdinalIgnoreCase))
switch (objective.Option.ToLowerInvariant())
{
case "pumpout":
#if SERVER
if (objective.Override || FlowPercentage > 0.0f)
{
item.CreateServerEvent(this);
}
if (objective.Override || !IsActive || FlowPercentage > -100.0f)
{
item.CreateServerEvent(this);
}
#endif
IsActive = false;
FlowPercentage = 0.0f;
}
else
{
IsActive = true;
FlowPercentage = -100.0f;
break;
case "pumpin":
#if SERVER
if (objective.Override || !IsActive || FlowPercentage > -100.0f)
{
item.CreateServerEvent(this);
}
if (objective.Override || !IsActive || FlowPercentage < 100.0f)
{
item.CreateServerEvent(this);
}
#endif
IsActive = true;
FlowPercentage = -100.0f;
IsActive = true;
FlowPercentage = 100.0f;
break;
case "stoppumping":
#if SERVER
if (objective.Override || FlowPercentage > 0.0f)
{
item.CreateServerEvent(this);
}
#endif
IsActive = false;
FlowPercentage = 0.0f;
break;
}
return true;
}
@@ -12,7 +12,6 @@ namespace Barotrauma.Items.Components
partial class Reactor : Powered, IServerSerializable, IClientSerializable
{
const float NetworkUpdateIntervalHigh = 0.5f;
const float NetworkUpdateIntervalLow = 10.0f;
//the rate at which the reactor is being run on (higher rate -> higher temperature)
private float fissionRate;
@@ -38,9 +37,8 @@ namespace Barotrauma.Items.Components
private float maxPowerOutput;
private Queue<float> loadQueue = new Queue<float>();
private float load;
private readonly Queue<float> loadQueue = new Queue<float>();
private bool unsentChanges;
private float sendUpdateTimer;
@@ -158,11 +156,6 @@ namespace Barotrauma.Items.Components
set { /*do nothing*/ }
}
private float correctTurbineOutput;
private float targetFissionRate;
private float targetTurbineOutput;
[Serialize(false, true, description: "Is the automatic temperature control currently on. Indended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
public bool AutoTemp
{
@@ -181,6 +174,18 @@ namespace Barotrauma.Items.Components
[Serialize(0.0f, true)]
public float AvailableFuel { get; set; }
[Serialize(0.0f, true)]
public new float Load { get; private set; }
[Serialize(0.0f, true)]
public float TargetFissionRate { get; set; }
[Serialize(0.0f, true)]
public float TargetTurbineOutput { get; set; }
[Serialize(0.0f, true)]
public float CorrectTurbineOutput { get; set; }
public Reactor(Item item, XElement element)
: base(item, element)
{
@@ -199,8 +204,8 @@ namespace Barotrauma.Items.Components
{
GameServer.Log(GameServer.CharacterLogName(lastUser) + " adjusted reactor settings: " +
"Temperature: " + (int)(temperature * 100.0f) +
", Fission rate: " + (int)targetFissionRate +
", Turbine output: " + (int)targetTurbineOutput +
", Fission rate: " + (int)TargetFissionRate +
", Turbine output: " + (int)TargetTurbineOutput +
(autoTemp ? ", Autotemp ON" : ", Autotemp OFF"),
ServerLog.MessageType.ItemInteraction);
@@ -223,7 +228,7 @@ namespace Barotrauma.Items.Components
}
#if CLIENT
if(PowerOn && AvailableFuel < 1)
if (PowerOn && AvailableFuel < 1)
{
HintManager.OnReactorOutOfFuel(this);
}
@@ -236,15 +241,15 @@ namespace Barotrauma.Items.Components
//so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily
if (!MathUtils.NearlyEqual(MaxPowerOutput, 0.0f))
{
correctTurbineOutput += MathHelper.Clamp((load / MaxPowerOutput * 100.0f) - correctTurbineOutput, -10.0f, 10.0f) * deltaTime;
CorrectTurbineOutput += MathHelper.Clamp((Load / MaxPowerOutput * 100.0f) - CorrectTurbineOutput, -10.0f, 10.0f) * deltaTime;
}
//calculate tolerances of the meters based on the skills of the user
//more skilled characters have larger "sweet spots", making it easier to keep the power output at a suitable level
float tolerance = MathHelper.Lerp(2.5f, 10.0f, degreeOfSuccess);
optimalTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
optimalTurbineOutput = new Vector2(CorrectTurbineOutput - tolerance, CorrectTurbineOutput + tolerance);
tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess);
allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
allowedTurbineOutput = new Vector2(CorrectTurbineOutput - tolerance, CorrectTurbineOutput + tolerance);
optimalTemperature = Vector2.Lerp(new Vector2(40.0f, 60.0f), new Vector2(30.0f, 70.0f), degreeOfSuccess);
allowedTemperature = Vector2.Lerp(new Vector2(30.0f, 70.0f), new Vector2(10.0f, 90.0f), degreeOfSuccess);
@@ -260,9 +265,9 @@ namespace Barotrauma.Items.Components
Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff));
//if (item.InWater && AvailableFuel < 100.0f) Temperature -= 12.0f * deltaTime;
FissionRate = MathHelper.Lerp(fissionRate, Math.Min(targetFissionRate, AvailableFuel), deltaTime);
FissionRate = MathHelper.Lerp(fissionRate, Math.Min(TargetFissionRate, AvailableFuel), deltaTime);
TurbineOutput = MathHelper.Lerp(turbineOutput, targetTurbineOutput, deltaTime);
TurbineOutput = MathHelper.Lerp(turbineOutput, TargetTurbineOutput, deltaTime);
float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f);
currPowerConsumption = -MaxPowerOutput * Math.Min(turbineOutput / 100.0f, temperatureFactor);
@@ -276,7 +281,7 @@ namespace Barotrauma.Items.Components
float maxAutoAdjust = maxPowerOutput * 0.1f;
autoAdjustAmount = MathHelper.Lerp(
autoAdjustAmount,
MathHelper.Clamp(-load - currPowerConsumption, -maxAutoAdjust, maxAutoAdjust),
MathHelper.Clamp(-Load - currPowerConsumption, -maxAutoAdjust, maxAutoAdjust),
deltaTime * 10.0f);
}
else
@@ -287,8 +292,8 @@ namespace Barotrauma.Items.Components
if (!PowerOn)
{
targetFissionRate = 0.0f;
targetTurbineOutput = 0.0f;
TargetFissionRate = 0.0f;
TargetTurbineOutput = 0.0f;
}
else if (autoTemp)
{
@@ -317,56 +322,30 @@ namespace Barotrauma.Items.Components
}
}
if (!loadQueue.Any() && PowerOn)
{
//loadQueue is empty, round must've just started
//reset the fission rate, turbine output and
//temperature to optimal levels to prevent fires
//at the start of the round
correctTurbineOutput = MathUtils.NearlyEqual(MaxPowerOutput, 0.0f) ? 0.0f : currentLoad / MaxPowerOutput * 100.0f;
tolerance = MathHelper.Lerp(2.5f, 10.0f, degreeOfSuccess);
optimalTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess);
allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
DebugConsole.Log($"Degree of success: {degreeOfSuccess}");
DebugConsole.Log($"Current load: {currentLoad}");
DebugConsole.Log($"Max power output: {MaxPowerOutput}");
DebugConsole.Log($"Available fuel: {AvailableFuel}");
float desiredTurbineOutput = MathHelper.Clamp(correctTurbineOutput, 0.0f, 100.0f);
DebugConsole.Log($"Turbine output reset: {targetTurbineOutput}, {turbineOutput} -> {desiredTurbineOutput}");
targetTurbineOutput = desiredTurbineOutput;
turbineOutput = desiredTurbineOutput;
float desiredTemperature = (optimalTemperature.X + optimalTemperature.Y) / 2.0f;
DebugConsole.Log($"Temperature reset: {temperature} -> {desiredTemperature}");
temperature = desiredTemperature;
float desiredFissionRate = GetFissionRateForTargetTemperatureAndTurbineOutput(desiredTemperature, desiredTurbineOutput);
DebugConsole.Log($"Fission rate reset: {targetFissionRate}, {fissionRate} -> {desiredFissionRate}");
targetFissionRate = desiredFissionRate;
fissionRate = desiredFissionRate;
}
loadQueue.Enqueue(currentLoad);
while (loadQueue.Count() > 60.0f)
{
load = loadQueue.Average();
Load = loadQueue.Average();
loadQueue.Dequeue();
}
float fuelLeft = 0.0f;
var containedItems = item.OwnInventory?.AllItems;
if (containedItems != null)
{
foreach (Item item in containedItems)
{
if (!item.HasTag("reactorfuel")) { continue; }
if (fissionRate > 0.0f)
{
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
}
fuelLeft += item.ConditionPercentage;
}
}
if (fissionRate > 0.0f)
{
var containedItems = item.OwnInventory?.AllItems;
if (containedItems != null)
{
foreach (Item item in containedItems)
{
if (!item.HasTag("reactorfuel")) { continue; }
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
}
}
if (item.AiTarget != null && MaxPowerOutput > 0)
{
var aiTarget = item.AiTarget;
@@ -385,8 +364,9 @@ namespace Barotrauma.Items.Components
item.SendSignal(((int)(temperature * 100.0f)).ToString(), "temperature_out");
item.SendSignal(((int)-CurrPowerConsumption).ToString(), "power_value_out");
item.SendSignal(((int)load).ToString(), "load_value_out");
item.SendSignal(((int)Load).ToString(), "load_value_out");
item.SendSignal(((int)AvailableFuel).ToString(), "fuel_out");
item.SendSignal(((int)fuelLeft).ToString(), "fuel_percentage_left");
UpdateFailures(deltaTime);
#if CLIENT
@@ -407,8 +387,7 @@ namespace Barotrauma.Items.Components
{
item.CreateServerEvent(this);
}
#endif
#if CLIENT
#elif CLIENT
if (GameMain.Client != null)
{
item.CreateClientEvent(this);
@@ -424,12 +403,6 @@ namespace Barotrauma.Items.Components
return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f;
}
private float GetFissionRateForTargetTemperatureAndTurbineOutput(float temperature, float turbineOutput)
{
if (MathUtils.NearlyEqual(AvailableFuel, 0f)) { return 0f; }
return (temperature + turbineOutput) / (AvailableFuel / 100f) / 2f;
}
/// <summary>
/// Do we need more fuel to generate enough power to match the current load.
/// </summary>
@@ -438,7 +411,7 @@ namespace Barotrauma.Items.Components
private bool NeedMoreFuel(float minimumOutputRatio, float minCondition = 0)
{
float remainingFuel = item.ContainedItems.Sum(i => i.Condition);
if (remainingFuel <= minCondition && load > 0.0f)
if (remainingFuel <= minCondition && Load > 0.0f)
{
return true;
}
@@ -455,7 +428,7 @@ namespace Barotrauma.Items.Components
float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * MaxPowerOutput;
//maximum output not enough, we need more fuel
return theoreticalMaxOutput < load * minimumOutputRatio;
return theoreticalMaxOutput < Load * minimumOutputRatio;
}
private bool TooMuchFuel()
@@ -467,7 +440,7 @@ namespace Barotrauma.Items.Components
float minimumHeat = GetGeneratedHeat(optimalFissionRate.X);
//if we need a very high turbine output to keep the engine from overheating, there's too much fuel
return minimumHeat > Math.Min(correctTurbineOutput * 1.5f, 90);
return minimumHeat > Math.Min(CorrectTurbineOutput * 1.5f, 90);
}
private void UpdateFailures(float deltaTime)
@@ -514,26 +487,26 @@ namespace Barotrauma.Items.Components
public void UpdateAutoTemp(float speed, float deltaTime)
{
float desiredTurbineOutput = (optimalTurbineOutput.X + optimalTurbineOutput.Y) / 2.0f;
targetTurbineOutput += MathHelper.Clamp(desiredTurbineOutput - targetTurbineOutput, -speed, speed) * deltaTime;
targetTurbineOutput = MathHelper.Clamp(targetTurbineOutput, 0.0f, 100.0f);
TargetTurbineOutput += MathHelper.Clamp(desiredTurbineOutput - TargetTurbineOutput, -speed, speed) * deltaTime;
TargetTurbineOutput = MathHelper.Clamp(TargetTurbineOutput, 0.0f, 100.0f);
float desiredFissionRate = (optimalFissionRate.X + optimalFissionRate.Y) / 2.0f;
targetFissionRate += MathHelper.Clamp(desiredFissionRate - targetFissionRate, -speed, speed) * deltaTime;
TargetFissionRate += MathHelper.Clamp(desiredFissionRate - TargetFissionRate, -speed, speed) * deltaTime;
if (temperature > (optimalTemperature.X + optimalTemperature.Y) / 2.0f)
{
targetFissionRate = Math.Min(targetFissionRate - speed * 2 * deltaTime, allowedFissionRate.Y);
TargetFissionRate = Math.Min(TargetFissionRate - speed * 2 * deltaTime, allowedFissionRate.Y);
}
else if (-currPowerConsumption < load)
else if (-currPowerConsumption < Load)
{
targetFissionRate = Math.Min(targetFissionRate + speed * 2 * deltaTime, 100.0f);
TargetFissionRate = Math.Min(TargetFissionRate + speed * 2 * deltaTime, 100.0f);
}
targetFissionRate = MathHelper.Clamp(targetFissionRate, 0.0f, 100.0f);
TargetFissionRate = MathHelper.Clamp(TargetFissionRate, 0.0f, 100.0f);
//don't push the target too far from the current fission rate
//otherwise we may "overshoot", cranking the target fission rate all the way up because it takes a while
//for the actual fission rate and temperature to follow
targetFissionRate = MathHelper.Clamp(targetFissionRate, FissionRate - 5, FissionRate + 5);
TargetFissionRate = MathHelper.Clamp(TargetFissionRate, FissionRate - 5, FissionRate + 5);
}
public void PowerUpImmediately()
@@ -557,8 +530,8 @@ namespace Barotrauma.Items.Components
currPowerConsumption = 0.0f;
Temperature -= deltaTime * 1000.0f;
targetFissionRate = Math.Max(targetFissionRate - deltaTime * 10.0f, 0.0f);
targetTurbineOutput = Math.Max(targetTurbineOutput - deltaTime * 10.0f, 0.0f);
TargetFissionRate = Math.Max(TargetFissionRate - deltaTime * 10.0f, 0.0f);
TargetTurbineOutput = Math.Max(TargetTurbineOutput - deltaTime * 10.0f, 0.0f);
#if CLIENT
FissionRateScrollBar.BarScroll = 1.0f - FissionRate / 100.0f;
TurbineOutputScrollBar.BarScroll = 1.0f - TurbineOutput / 100.0f;
@@ -583,7 +556,6 @@ namespace Barotrauma.Items.Components
containedItem.Condition = 0.0f;
}
}
#if SERVER
GameServer.Log("Reactor meltdown!", ServerLog.MessageType.ItemInteraction);
if (GameMain.Server != null)
@@ -696,15 +668,15 @@ namespace Barotrauma.Items.Components
bool prevAutoTemp = autoTemp;
bool prevPowerOn = _powerOn;
float prevFissionRate = targetFissionRate;
float prevTurbineOutput = targetTurbineOutput;
float prevFissionRate = TargetFissionRate;
float prevTurbineOutput = TargetTurbineOutput;
if (shutDown)
{
PowerOn = false;
AutoTemp = false;
targetFissionRate = 0.0f;
targetTurbineOutput = 0.0f;
TargetFissionRate = 0.0f;
TargetTurbineOutput = 0.0f;
unsentChanges = true;
return true;
}
@@ -730,8 +702,8 @@ namespace Barotrauma.Items.Components
#endif
if (autoTemp != prevAutoTemp ||
prevPowerOn != _powerOn ||
Math.Abs(prevFissionRate - targetFissionRate) > 1.0f ||
Math.Abs(prevTurbineOutput - targetTurbineOutput) > 1.0f)
Math.Abs(prevFissionRate - TargetFissionRate) > 1.0f ||
Math.Abs(prevTurbineOutput - TargetTurbineOutput) > 1.0f)
{
unsentChanges = true;
}
@@ -767,32 +739,32 @@ namespace Barotrauma.Items.Components
switch (connection.Name)
{
case "shutdown":
if (targetFissionRate > 0.0f || targetTurbineOutput > 0.0f)
if (TargetFissionRate > 0.0f || TargetTurbineOutput > 0.0f)
{
PowerOn = false;
AutoTemp = false;
targetFissionRate = 0.0f;
targetTurbineOutput = 0.0f;
TargetFissionRate = 0.0f;
TargetTurbineOutput = 0.0f;
if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
}
break;
case "set_fissionrate":
if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate))
{
targetFissionRate = MathHelper.Clamp(newFissionRate, 0.0f, 100.0f);
TargetFissionRate = MathHelper.Clamp(newFissionRate, 0.0f, 100.0f);
if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
#if CLIENT
FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f;
FissionRateScrollBar.BarScroll = TargetFissionRate / 100.0f;
#endif
}
break;
case "set_turbineoutput":
if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput))
{
targetTurbineOutput = MathHelper.Clamp(newTurbineOutput, 0.0f, 100.0f);
TargetTurbineOutput = MathHelper.Clamp(newTurbineOutput, 0.0f, 100.0f);
if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
#if CLIENT
TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f;
TurbineOutputScrollBar.BarScroll = TargetTurbineOutput / 100.0f;
#endif
}
break;
@@ -17,6 +17,8 @@ namespace Barotrauma.Items.Components
public const float DefaultSonarRange = 10000.0f;
public const float PassivePowerConsumption = 0.1f;
class ConnectedTransducer
{
public readonly SonarTransducer Transducer;
@@ -150,7 +152,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
currPowerConsumption = (currentMode == Mode.Active) ? powerConsumption : powerConsumption * 0.1f;
currPowerConsumption = (currentMode == Mode.Active) ? powerConsumption : powerConsumption * PassivePowerConsumption;
UpdateOnActiveEffects(deltaTime);
@@ -332,7 +334,9 @@ namespace Barotrauma.Items.Components
if (connection.Name == "transducer_in")
{
var transducer = signal.source.GetComponent<SonarTransducer>();
if (transducer == null) return;
if (transducer == null) { return; }
transducer.ConnectedSonar = this;
var connectedTransducer = connectedTransducers.Find(t => t.Transducer == transducer);
if (connectedTransducer == null)
@@ -8,6 +8,8 @@ namespace Barotrauma.Items.Components
private float sendSignalTimer;
public Sonar ConnectedSonar;
public SonarTransducer(Item item, XElement element) : base(item, element)
{
IsActive = true;
@@ -17,7 +19,7 @@ namespace Barotrauma.Items.Components
{
UpdateOnActiveEffects(deltaTime);
CurrPowerConsumption = powerConsumption;
CurrPowerConsumption = powerConsumption * (ConnectedSonar?.CurrentMode == Sonar.Mode.Active ? 1.0f : Sonar.PassivePowerConsumption);
if (Voltage >= MinVoltage)
{
@@ -61,6 +61,8 @@ namespace Barotrauma.Items.Components
public List<Body> IgnoredBodies;
private Character stickTargetCharacter;
private Character _user;
public Character User
{
@@ -614,8 +616,8 @@ namespace Barotrauma.Items.Components
return;
}
//target very far from the item -> update the item's transform to make sure it's inside the same sub as the target (or outside)
if (Math.Abs(stickJoint.JointTranslation) > 100.0f)
// Update the item's transform to make sure it's inside the same sub as the target (or outside)
if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || Math.Abs(stickJoint.JointTranslation) > 100.0f)
{
item.UpdateTransform();
}
@@ -866,7 +868,7 @@ namespace Barotrauma.Items.Components
(DoesStick ||
(StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
(StickToStructures && target.Body.UserData is Structure) ||
(StickToItems && target.Body.UserData is Item)))
(StickToItems && target.Body.UserData is Item)))
{
Vector2 dir = new Vector2(
(float)Math.Cos(item.body.Rotation),
@@ -965,9 +967,14 @@ namespace Barotrauma.Items.Components
GameMain.World.Add(stickJoint);
IsActive = true;
if (targetBody.UserData is Limb limb)
{
stickTargetCharacter = limb.character;
stickTargetCharacter.AttachedProjectiles.Add(this);
}
}
private void Unstick()
public void Unstick()
{
StickTarget = null;
if (stickJoint != null)
@@ -979,25 +986,21 @@ namespace Barotrauma.Items.Components
stickJoint = null;
}
if (!item.body.FarseerBody.IsBullet) { IsActive = false; }
item.GetComponent<Rope>()?.Snap();
if (stickTargetCharacter != null)
{
stickTargetCharacter.AttachedProjectiles.Remove(this);
stickTargetCharacter = null;
}
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
if (stickJoint != null)
if (IsStuckToTarget || stickJoint != null || stickTargetCharacter != null)
{
try
{
GameMain.World.Remove(stickJoint);
}
catch
{
//the body that the projectile was stuck to has been removed
}
stickJoint = null;
Unstick();
}
}
partial void LaunchProjSpecific(Vector2 startLocation, Vector2 endLocation);
}
@@ -26,6 +26,10 @@ namespace Barotrauma.Items.Components
RepairToolStructureRepairMultiplier,
RepairToolStructureDamageMultiplier,
RepairToolDeattachTimeMultiplier,
StoppingPowerMultiplier,
StrikingPowerMultiplier,
StrikingSpeedMultiplier,
FiringRateMultiplier,
// unused as of now
AttackMultiplier,
AttackSpeedMultiplier,
@@ -33,7 +37,6 @@ namespace Barotrauma.Items.Components
RangedSpreadReduction,
ChargeSpeedMultiplier,
MovementSpeedMultiplier,
// generic stats to be used for various needs, declared just in case (localization)
EffectivenessMultiplier,
PowerOutputMultiplier,
ConsumptionReductionMultiplier,
@@ -105,11 +105,6 @@ namespace Barotrauma.Items.Components
public bool IsTinkering { get; private set; } = false;
public float RepairIconThreshold
{
get { return RepairThreshold / 2; }
}
public Character CurrentFixer { get; private set; }
private Item currentRepairItem;
@@ -118,6 +113,9 @@ namespace Barotrauma.Items.Components
public float TinkeringStrength => tinkeringStrength;
public bool IsBelowRepairThreshold => item.ConditionPercentage <= RepairThreshold;
public bool IsBelowRepairIconThreshold => item.ConditionPercentage <= RepairThreshold / 2;
public enum FixActions : int
{
None = 0,
@@ -179,8 +177,17 @@ namespace Barotrauma.Items.Components
if (bestRepairItem != null && bestRepairItem.Prefab.CannotRepairFail) { return true; }
// unpowered (electrical) items can be repaired without a risk of electrical shock
if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase)) &&
item.GetComponent<Powered>() is Powered powered && powered.Voltage < 0.1f) { return true; }
if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase)))
{
if (item.GetComponent<Reactor>() is Reactor reactor)
{
if (MathUtils.NearlyEqual(reactor.CurrPowerConsumption, 0.0f, 0.1f)) { return true; }
}
else if (item.GetComponent<Powered>() is Powered powered && powered.Voltage < 0.1f)
{
return true;
}
}
if (Rand.Range(0.0f, 0.5f) < RepairDegreeOfSuccess(character, requiredSkills)) { return true; }
@@ -393,7 +400,7 @@ namespace Barotrauma.Items.Components
float successFactor = requiredSkills.Count == 0 ? 1.0f : RepairDegreeOfSuccess(CurrentFixer, requiredSkills);
//item must have been below the repair threshold for the player to get an achievement or XP for repairing it
if (item.ConditionPercentage < RepairThreshold)
if (IsBelowRepairThreshold)
{
wasBroken = true;
}
@@ -524,7 +531,7 @@ namespace Barotrauma.Items.Components
public void AdjustPowerConsumption(ref float powerConsumption)
{
if (item.ConditionPercentage < RepairThreshold)
if (IsBelowRepairThreshold)
{
powerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
}
@@ -306,9 +306,13 @@ namespace Barotrauma.Items.Components
forceDir.X = Math.Clamp(forceDir.X, -0.1f, 0.1f);
}
}
float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance)) : TargetPullForce;
float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce;
targetBody?.ApplyForce(-forceDir * force);
targetCharacter?.AnimController.Collider.ApplyForce(-forceDir * force * 3);
var targetRagdoll = targetCharacter?.AnimController;
if (targetRagdoll != null && (targetRagdoll.InWater || targetRagdoll.OnGround))
{
targetRagdoll.Collider.ApplyForce(-forceDir * force * 3);
}
}
}
}
@@ -19,6 +19,10 @@ namespace Barotrauma.Items.Components
get { return timeFrame; }
set
{
if (value > timeFrame)
{
timeSinceReceived[0] = timeSinceReceived[1] = Math.Max(value * 2.0f, 0.1f);
}
timeFrame = Math.Max(0.0f, value);
}
}
@@ -39,6 +39,10 @@ namespace Barotrauma.Items.Components
get { return timeFrame; }
set
{
if (value > timeFrame)
{
timeSinceReceived[0] = timeSinceReceived[1] = Math.Max(value * 2.0f, 0.1f);
}
timeFrame = Math.Max(0.0f, value);
}
}
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
class DelayComponent : ItemComponent
@@ -114,6 +114,18 @@ namespace Barotrauma.Items.Components
};
signalQueue.Enqueue(prevQueuedSignal);
break;
case "set_delay":
if (float.TryParse(signal.value, out float newDelay))
{
newDelay = MathHelper.Clamp(newDelay, 0, 60);
if (signalQueue.Count > 0 && newDelay != Delay)
{
prevQueuedSignal = null;
signalQueue.Clear();
}
Delay = newDelay;
}
break;
}
}
}
@@ -62,6 +62,10 @@ namespace Barotrauma.Items.Components
get { return timeFrame; }
set
{
if (value > timeFrame)
{
timeSinceReceived[0] = timeSinceReceived[1] = Math.Max(value * 2.0f, 0.1f);
}
timeFrame = Math.Max(0.0f, value);
}
}
@@ -43,7 +43,16 @@ namespace Barotrauma.Items.Components
}
}
public float Rotation;
private float rotation;
public float Rotation
{
get { return rotation; }
set
{
rotation = value;
SetLightSourceTransform();
}
}
[Editable, Serialize(true, true, description: "Should structures cast shadows when light from this light source hits them. " +
"Disabling shadows increases the performance of the game, and is recommended for lights with a short range.", alwaysUseInstanceValues: true)]
@@ -246,39 +255,14 @@ namespace Barotrauma.Items.Components
SetLightSourceState(false, 0.0f);
return;
}
#if CLIENT
if (ParentBody != null)
{
Light.Position = ParentBody.Position;
}
else if (turret != null)
{
Light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y);
}
else
{
Light.Position = item.Position;
}
#endif
SetLightSourceTransform();
PhysicsBody body = ParentBody ?? item.body;
if (body != null)
if (body != null && !body.Enabled)
{
#if CLIENT
Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi;
Light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically;
#endif
if (!body.Enabled)
{
SetLightSourceState(false, 0.0f);
return;
}
}
else
{
#if CLIENT
Light.Rotation = -Rotation - MathHelper.ToRadians(item.Rotation);
Light.LightSpriteEffect = item.SpriteEffects;
#endif
SetLightSourceState(false, 0.0f);
return;
}
currPowerConsumption = powerConsumption;
@@ -333,6 +317,9 @@ namespace Barotrauma.Items.Components
if (signal.value != prevColorSignal)
{
LightColor = XMLExtensions.ParseColor(signal.value, false);
#if CLIENT
SetLightSourceState(Light.Enabled, currentBrightness);
#endif
prevColorSignal = signal.value;
}
break;
@@ -350,5 +337,8 @@ namespace Barotrauma.Items.Components
}
partial void SetLightSourceState(bool enabled, float brightness);
partial void SetLightSourceTransform();
}
}
@@ -34,7 +34,12 @@ namespace Barotrauma.Items.Components
}
}
protected bool writeable = true;
[Editable, Serialize(true, true, description: "Can the value stored in the memory component be changed via signals.", alwaysUseInstanceValues: true)]
public bool Writeable
{
get;
set;
}
public MemoryComponent(Item item, XElement element)
: base(item, element)
@@ -54,7 +59,7 @@ namespace Barotrauma.Items.Components
switch (connection.Name)
{
case "signal_in":
if (writeable)
if (Writeable)
{
string prevValue = Value;
Value = signal.value;
@@ -66,7 +71,7 @@ namespace Barotrauma.Items.Components
break;
case "signal_store":
case "lock_state":
writeable = signal.value == "1";
Writeable = signal.value == "1";
break;
}
}
@@ -131,6 +131,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, true, description: "Should the sensor trigger when the item itself moves.")]
public bool DetectOwnMotion
{
get;
set;
}
public MotionSensor(Item item, XElement element)
: base(item, element)
{
@@ -168,7 +175,7 @@ namespace Barotrauma.Items.Components
MotionDetected = false;
updateTimer = UpdateInterval;
if (item.body != null && item.body.Enabled)
if (item.body != null && item.body.Enabled && DetectOwnMotion)
{
if (Math.Abs(item.body.LinearVelocity.X) > MinimumVelocity || Math.Abs(item.body.LinearVelocity.Y) > MinimumVelocity)
{
@@ -6,6 +6,8 @@ namespace Barotrauma.Items.Components
{
class RegExFindComponent : ItemComponent
{
private static readonly TimeSpan timeout = TimeSpan.FromSeconds(Timing.Step);
private string expression;
private string receivedSignal;
@@ -67,7 +69,10 @@ namespace Barotrauma.Items.Components
try
{
regex = new Regex(@expression);
regex = new Regex(
@expression,
options: RegexOptions.None,
matchTimeout: timeout);
}
catch
@@ -97,11 +102,14 @@ namespace Barotrauma.Items.Components
previousResult = match.Success;
previousGroups = UseCaptureGroup && previousResult ? match.Groups : null;
previousReceivedSignal = receivedSignal;
}
catch
catch (Exception e)
{
item.SendSignal("ERROR", "signal_out");
item.SendSignal(
e is RegexMatchTimeoutException
? "TIMEOUT"
: "ERROR",
"signal_out");
previousResult = false;
return;
}
@@ -24,6 +24,10 @@ namespace Barotrauma.Items.Components
get { return timeFrame; }
set
{
if (value > timeFrame)
{
timeSinceReceived[0] = timeSinceReceived[1] = Math.Max(value * 2.0f, 0.1f);
}
timeFrame = Math.Max(0.0f, value);
}
}
@@ -2,16 +2,35 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
readonly struct TerminalMessage
{
public readonly string Text;
public readonly Color Color;
public TerminalMessage(string text, Color color)
{
Text = text;
Color = color;
}
public void Deconstruct(out string text, out Color color)
{
text = Text;
color = Color;
}
}
partial class Terminal : ItemComponent
{
private const int MaxMessageLength = ChatMessage.MaxLength;
private const int MaxMessages = 60;
private List<string> messageHistory = new List<string>(MaxMessages);
private List<TerminalMessage> messageHistory = new List<TerminalMessage>(MaxMessages);
public string DisplayedWelcomeMessage
{
@@ -37,19 +56,39 @@ namespace Barotrauma.Items.Components
/// </summary>
public string ShowMessage
{
get { return messageHistory.Count == 0 ? string.Empty : messageHistory.Last(); }
get { return messageHistory.Count == 0 ? string.Empty : messageHistory.Last().Text; }
set
{
if (string.IsNullOrEmpty(value)) { return; }
ShowOnDisplay(value, addToHistory: true);
ShowOnDisplay(value, addToHistory: true, TextColor);
}
}
[Editable, Serialize(false, true, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)]
public bool UseMonospaceFont { get; set; }
private Color textColor = Color.LimeGreen;
[Editable, Serialize("50,205,50,255", true, description: "Color of the terminal text.", alwaysUseInstanceValues: true)]
public Color TextColor
{
get => textColor;
set
{
textColor = value;
#if CLIENT
if (inputBox is { } input)
{
input.TextColor = value;
}
#endif
}
}
private string OutputValue { get; set; }
private string prevColorSignal;
public Terminal(Item item, XElement element)
: base(item, element)
{
@@ -59,18 +98,39 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific(XElement element);
partial void ShowOnDisplay(string input, bool addToHistory);
partial void ShowOnDisplay(string input, bool addToHistory, Color color);
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (connection.Name != "signal_in") { return; }
if (signal.value.Length > MaxMessageLength)
switch (connection.Name)
{
signal.value = signal.value.Substring(0, MaxMessageLength);
}
case "set_text":
string inputSignal = signal.value.Replace("\\n", "\n");
ShowOnDisplay(inputSignal, addToHistory: true);
if (signal.value.Length > MaxMessageLength)
{
signal.value = signal.value.Substring(0, MaxMessageLength);
}
string inputSignal = signal.value.Replace("\\n", "\n");
ShowOnDisplay(inputSignal, addToHistory: true, TextColor);
break;
case "set_text_color":
if (signal.value != prevColorSignal)
{
TextColor = XMLExtensions.ParseColor(signal.value, false);
prevColorSignal = signal.value;
}
break;
case "clear_text" when signal.value != "0":
messageHistory.Clear();
#if CLIENT
if (historyBox?.Content is { } history)
{
history.ClearChildren();
}
#endif
break;
}
}
public override void OnItemLoaded()
@@ -83,7 +143,7 @@ namespace Barotrauma.Items.Components
base.OnItemLoaded();
if (!string.IsNullOrEmpty(DisplayedWelcomeMessage))
{
ShowOnDisplay(DisplayedWelcomeMessage, addToHistory: !isSubEditor);
ShowOnDisplay(DisplayedWelcomeMessage, addToHistory: !isSubEditor, TextColor);
DisplayedWelcomeMessage = "";
//remove welcome message if a game session is running so it doesn't reappear on successive rounds
if (GameMain.GameSession != null && !isSubEditor)
@@ -98,7 +158,8 @@ namespace Barotrauma.Items.Components
var componentElement = base.Save(parentElement);
for (int i = 0; i < messageHistory.Count; i++)
{
componentElement.Add(new XAttribute("msg" + i, messageHistory[i]));
componentElement.Add(new XAttribute("msg" + i, messageHistory[i].Text));
componentElement.Add(new XAttribute("color" + i, messageHistory[i].Color.ToStringHex()));
}
return componentElement;
}
@@ -109,8 +170,9 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < MaxMessages; i++)
{
string msg = componentElement.GetAttributeString("msg" + i, null);
if (msg == null) { break; }
ShowOnDisplay(msg, addToHistory: true);
if (msg is null) { break; }
Color color = componentElement.GetAttributeColor("color" + i, TextColor);
ShowOnDisplay(msg, addToHistory: true, color);
}
}
}
@@ -17,8 +17,8 @@ namespace Barotrauma.Items.Components
Atan,
}
private float[] receivedSignal = new float[2];
private float[] timeSinceReceived = new float[2];
private readonly float[] receivedSignal = new float[2];
private readonly float[] timeSinceReceived = new float[2];
[Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)]
public FunctionType Function
@@ -125,7 +125,17 @@ namespace Barotrauma.Items.Components
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication)
{
return false;
}
}
//if the component is not linked to chat and has nothing connected to the output, sending a signal to it does nothing
// = no point in receiving
if (!LinkToChat)
{
if (signalOutConnection == null || !signalOutConnection.Wires.Any(w => w != null))
{
return false;
}
}
if (Vector2.DistanceSquared(item.WorldPosition, sender.item.WorldPosition) > sender.range * sender.range) { return false; }
@@ -9,7 +9,7 @@ using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class TriggerComponent : ItemComponent
partial class TriggerComponent : ItemComponent
{
[Editable, Serialize(0.0f, true, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
public float Force { get; set; }
@@ -18,12 +18,32 @@ namespace Barotrauma.Items.Components
private float Radius { get; set; }
private float RadiusInDisplayUnits { get; set; }
private bool TriggeredOnce { get; set; }
private float CurrentForceFluctuation { get; set; } = 1.0f;
public bool TriggerActive { get; private set; }
private float ForceFluctuationTimer { get; set; }
private static float TimeInLevel
{
get
{
if (GameMain.GameSession != null)
{
return (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime);
}
else
{
return 0.0f;
}
}
}
private readonly LevelTrigger.TriggererType triggeredBy;
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
private readonly bool triggerOnce;
private readonly bool distanceBasedForce;
private readonly bool forceFluctuation;
private readonly float forceFluctuationStrength;
private readonly float forceFluctuationFrequency;
private readonly float forceFluctuationInterval;
private readonly List<ISerializableEntity> statusEffectTargets = new List<ISerializableEntity>();
/// <summary>
/// Effects applied to entities inside the trigger
@@ -42,6 +62,15 @@ namespace Barotrauma.Items.Components
DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type.");
}
triggerOnce = element.GetAttributeBool("triggeronce", false);
distanceBasedForce = element.GetAttributeBool("distancebasedforce", false);
forceFluctuation = element.GetAttributeBool("forcefluctuation", false);
forceFluctuationStrength = element.GetAttributeFloat("forcefluctuationstrength", 1.0f);
forceFluctuationStrength = Math.Clamp(forceFluctuationStrength, 0.0f, 1.0f);
forceFluctuationFrequency = element.GetAttributeFloat("fluctuationfrequency", 1.0f);
forceFluctuationFrequency = Math.Max(forceFluctuationFrequency, 0.01f);
forceFluctuationInterval = element.GetAttributeFloat("fluctuationinterval", 0.01f);
forceFluctuationInterval = Math.Max(forceFluctuationInterval, 0.01f);
string parentDebugName = $"TriggerComponent in {item.Name}";
foreach (XElement subElement in element.Elements())
{
@@ -128,6 +157,19 @@ namespace Barotrauma.Items.Components
TriggerActive = triggerers.Any();
if (forceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
ForceFluctuationTimer += deltaTime;
if (ForceFluctuationTimer >= forceFluctuationInterval)
{
float v = MathF.Sin(2 * MathF.PI * forceFluctuationFrequency * TimeInLevel);
float amount = MathUtils.InverseLerp(-1.0f, 1.0f, v);
CurrentForceFluctuation = MathHelper.Lerp(1.0f - forceFluctuationStrength, 1.0f, amount);
ForceFluctuationTimer = 0.0f;
GameMain.NetworkMember?.CreateEntityEvent(this);
}
}
foreach (Entity triggerer in triggerers)
{
LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets);
@@ -167,9 +209,9 @@ namespace Barotrauma.Items.Components
{
Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition);
if (diff.LengthSquared() < 0.0001f) { return; }
float distanceFactor = LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits);
float distanceFactor = distanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f;
if (distanceFactor <= 0.0f) { return; }
Vector2 force = distanceFactor * Force * Vector2.Normalize(diff);
Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff);
if (force.LengthSquared() < 0.01f) { return; }
body.ApplyForce(force);
}
@@ -68,6 +68,9 @@ namespace Barotrauma.Items.Components
private const float TinkeringDamageIncrease = 0.2f;
private const float TinkeringReloadDecrease = 0.2f;
public Character ActiveUser;
private float resetActiveUserTimer;
public float Rotation
{
get { return rotation; }
@@ -362,7 +365,7 @@ namespace Barotrauma.Items.Components
UpdateTransformedBarrelPos();
}
if (user != null && user.Removed)
if (user is { Removed: true })
{
user = null;
}
@@ -371,6 +374,19 @@ namespace Barotrauma.Items.Components
resetUserTimer -= deltaTime;
if (resetUserTimer <= 0.0f) { user = null; }
}
if (ActiveUser is { Removed: true })
{
ActiveUser = null;
}
else
{
resetActiveUserTimer -= deltaTime;
if (resetActiveUserTimer <= 0.0f)
{
ActiveUser = null;
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
@@ -744,8 +760,10 @@ namespace Barotrauma.Items.Components
if (projectileComponent != null)
{
projectileComponent.Attacker = projectileComponent.User = user;
projectileComponent.Attack.DamageMultiplier = 1f + (TinkeringDamageIncrease * tinkeringStrength);
if (projectileComponent.Attack != null)
{
projectileComponent.Attack.DamageMultiplier = 1f + (TinkeringDamageIncrease * tinkeringStrength);
}
projectileComponent.Use();
projectile.GetComponent<Rope>()?.Attach(item, projectile);
projectileComponent.User = user;
@@ -956,10 +974,11 @@ namespace Barotrauma.Items.Components
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget &&
previousTarget.IsDead)
if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead)
{
character.Speak(TextManager.Get("DialogTurretTargetDead"), identifier: "killedtarget" + previousTarget.ID, minDurationBetweenSimilar: 10.0f);
character.Speak(TextManager.Get("DialogTurretTargetDead"),
identifier: "killedtarget" + previousTarget.ID,
minDurationBetweenSimilar: 10.0f);
character.AIController.SelectTarget(null);
}
@@ -986,7 +1005,9 @@ namespace Barotrauma.Items.Components
}
else
{
character.Speak(TextManager.Get("DialogSupercapacitorIsBroken"), identifier: "supercapacitorisbroken", minDurationBetweenSimilar: 30.0f);
character.Speak(TextManager.Get("DialogSupercapacitorIsBroken"),
identifier: "supercapacitorisbroken",
minDurationBetweenSimilar: 30.0f);
canShoot = false;
}
}
@@ -999,7 +1020,9 @@ namespace Barotrauma.Items.Components
}
if (lowestCharge <= 0 && batteryToLoad.Item.ConditionPercentage > 0)
{
character.Speak(TextManager.Get("DialogTurretHasNoPower"), identifier: "turrethasnopower", minDurationBetweenSimilar: 30.0f);
character.Speak(TextManager.Get("DialogTurretHasNoPower"),
identifier: "turrethasnopower",
minDurationBetweenSimilar: 30.0f);
canShoot = false;
}
}
@@ -1039,7 +1062,9 @@ namespace Barotrauma.Items.Components
{
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: true), identifier: "cannotloadturret", minDurationBetweenSimilar: 30.0f);
character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: true),
identifier: "cannotloadturret",
minDurationBetweenSimilar: 30.0f);
}
return true;
}
@@ -1049,7 +1074,9 @@ namespace Barotrauma.Items.Components
loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier };
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: true), identifier: "loadturret", minDurationBetweenSimilar: 30.0f);
character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: true),
identifier: "loadturret",
minDurationBetweenSimilar: 30.0f);
}
loadItemsObjective.Abandoned += CheckRemainingAmmo;
loadItemsObjective.Completed += CheckRemainingAmmo;
@@ -1063,11 +1090,15 @@ namespace Barotrauma.Items.Components
int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1);
if (remainingAmmo == 0)
{
character.Speak(TextManager.Get($"DialogOutOf{ammoType}", fallBackTag: "DialogOutOfTurretAmmo"), identifier: "outofammo", minDurationBetweenSimilar: 30.0f);
character.Speak(TextManager.Get($"DialogOutOf{ammoType}", fallBackTag: "DialogOutOfTurretAmmo"),
identifier: "outofammo",
minDurationBetweenSimilar: 30.0f);
}
else if (remainingAmmo < 3)
{
character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), identifier: "outofammo", minDurationBetweenSimilar: 30.0f);
character.Speak(TextManager.Get($"DialogLowOn{ammoType}"),
identifier: "outofammo",
minDurationBetweenSimilar: 30.0f);
}
}
}
@@ -1090,7 +1121,8 @@ namespace Barotrauma.Items.Components
float closestDistance = maxDistance * maxDistance;
if (currentTarget != null)
bool hadCurrentTarget = currentTarget != null;
if (hadCurrentTarget)
{
if (currentTarget.Removed || currentTarget.IsDead)
{
@@ -1222,24 +1254,32 @@ namespace Barotrauma.Items.Components
{
if (character.IsOnPlayerTeam)
{
if (character.AIController.SelectedAiTarget == null)
if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget)
{
if (GameMain.Config.RecentlyEncounteredCreatures.Contains(closestEnemy.SpeciesName))
{
character.Speak(TextManager.Get("DialogNewTargetSpotted"), null, 0.0f, "newtargetspotted", 30.0f);
character.Speak(TextManager.Get("DialogNewTargetSpotted"),
identifier: "newtargetspotted",
minDurationBetweenSimilar: 30.0f);
}
else if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase)))
{
character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName), null, 0.0f, "identifiedtargetspotted", 30.0f);
character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName),
identifier: "identifiedtargetspotted",
minDurationBetweenSimilar: 30.0f);
}
else
{
character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), null, 0.0f, "unidentifiedtargetspotted", 5.0f);
character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"),
identifier: "unidentifiedtargetspotted",
minDurationBetweenSimilar: 5.0f);
}
}
else if (GameMain.Config.EncounteredCreatures.None(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase)))
{
character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), null, 0.0f, "unidentifiedtargetspotted", 5.0f);
character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"),
identifier: "unidentifiedtargetspotted",
minDurationBetweenSimilar: 5.0f);
}
character.AddEncounter(closestEnemy);
}
@@ -1247,7 +1287,9 @@ namespace Barotrauma.Items.Components
}
else if (closestEnemy == null && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogIceSpireSpotted"), null, 0.0f, "icespirespotted", 60.0f);
character.Speak(TextManager.Get("DialogIceSpireSpotted"),
identifier: "icespirespotted",
minDurationBetweenSimilar: 60.0f);
}
character.CursorPosition = targetPos.Value;
@@ -1289,7 +1331,9 @@ namespace Barotrauma.Items.Components
if (!shoot) { return false; }
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f);
character.Speak(TextManager.Get("DialogFireTurret"),
identifier: "fireturret",
minDurationBetweenSimilar: 30.0f);
}
character.SetInput(InputType.Shoot, true, true);
}
@@ -1389,6 +1433,7 @@ namespace Barotrauma.Items.Components
crosshairSprite?.Remove(); crosshairSprite = null;
crosshairPointerSprite?.Remove(); crosshairPointerSprite = null;
moveSoundChannel?.Dispose(); moveSoundChannel = null;
WeaponIndicatorSprite?.Remove(); WeaponIndicatorSprite = null;
#endif
}
@@ -1503,12 +1548,16 @@ namespace Barotrauma.Items.Components
IsActive = true;
}
user = sender;
ActiveUser = sender;
resetActiveUserTimer = 1f;
resetUserTimer = 10.0f;
break;
case "trigger_in":
if (signal.value == "0") { return; }
item.Use((float)Timing.Step, sender);
user = sender;
ActiveUser = sender;
resetActiveUserTimer = 1f;
resetUserTimer = 10.0f;
//triggering the Use method through item.Use will fail if the item is not characterusable and the signal was sent by a character
//so lets do it manually
@@ -1521,12 +1570,18 @@ namespace Barotrauma.Items.Components
if (lightComponent != null && signal.value != "0")
{
lightComponent.IsOn = !lightComponent.IsOn;
UpdateLightComponent();
}
break;
case "set_light":
if (lightComponent != null)
{
lightComponent.IsOn = signal.value != "0";
bool shouldBeOn = signal.value != "0";
if (shouldBeOn != lightComponent.IsOn)
{
lightComponent.IsOn = shouldBeOn;
UpdateLightComponent();
}
}
break;
}
@@ -802,13 +802,13 @@ namespace Barotrauma
if (otherIsEquipped)
{
existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent));
stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent));
existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent, ignoreCondition: true));
stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent, ignoreCondition: true));
}
else
{
stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent));
existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent));
stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent, ignoreCondition: true));
existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent, ignoreCondition: true));
}
#if CLIENT
@@ -473,7 +473,12 @@ namespace Barotrauma
public float HealthMultiplier
{
get => healthMultiplier;
set { healthMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); }
set
{
float prevConditionPercentage = ConditionPercentage;
healthMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity);
Condition = MaxCondition * prevConditionPercentage / 100.0f;
}
}
private float maxRepairConditionMultiplier = 1.0f;
@@ -564,21 +569,26 @@ namespace Barotrauma
public bool StolenDuringRound;
private bool spawnedInOutpost;
public bool SpawnedInOutpost
private bool spawnedInCurrentOutpost;
public bool SpawnedInCurrentOutpost
{
get { return spawnedInOutpost; }
get { return spawnedInCurrentOutpost; }
set
{
if (!spawnedInOutpost && value)
if (!spawnedInCurrentOutpost && value)
{
OriginalOutpost = GameMain.GameSession?.StartLocation?.BaseName ?? "";
}
spawnedInOutpost = value;
spawnedInCurrentOutpost = value;
}
}
public bool AllowStealing = true;
[Serialize(true, true, alwaysUseInstanceValues: true)]
public bool AllowStealing
{
get;
set;
}
private string originalOutpost;
[Serialize("", true, alwaysUseInstanceValues: true)]
@@ -588,9 +598,9 @@ namespace Barotrauma
set
{
originalOutpost = value;
if (!string.IsNullOrEmpty(value) && GameMain.GameSession?.StartLocation?.BaseName == value)
if (!string.IsNullOrEmpty(value) && GameMain.GameSession?.LevelData?.Type == LevelData.LevelType.Outpost && GameMain.GameSession?.StartLocation?.BaseName == value)
{
spawnedInOutpost = true;
spawnedInCurrentOutpost = true;
}
}
}
@@ -1736,10 +1746,10 @@ namespace Barotrauma
Submarine prevSub = Submarine;
var projectile = GetComponent<Projectile>();
if (projectile?.StickTarget?.UserData is Limb limb)
if (projectile?.StickTarget?.UserData is Limb limb && limb.character != null)
{
Submarine = body.Submarine = limb.character?.Submarine;
currentHull = limb.character?.CurrentHull;
Submarine = body.Submarine = limb.character.Submarine;
currentHull = limb.character.CurrentHull;
}
else
{
@@ -2125,7 +2135,7 @@ namespace Barotrauma
}
private IEnumerable<object> DelaySignal(Signal signal, Connection connection)
private IEnumerable<CoroutineStatus> DelaySignal(Signal signal, Connection connection)
{
do
{
@@ -103,6 +103,9 @@ namespace Barotrauma
container.IsActive = true;
container.OnItemContained(item);
#if SERVER
GameMain.Server?.KarmaManager?.OnItemContained(item, container.Item, user);
#endif
}
return wasPut;
@@ -122,6 +125,9 @@ namespace Barotrauma
container.IsActive = true;
container.OnItemContained(item);
#if SERVER
GameMain.Server?.KarmaManager?.OnItemContained(item, container.Item, user);
#endif
}
return wasPut;
@@ -45,7 +45,8 @@ namespace Barotrauma
{
DebugConsole.AddWarning($"Invalid deconstruction output in \"{parentDebugName}\": the output item \"{ItemIdentifier}\" has the out condition set, but is also set to copy the condition of the deconstructed item. Ignoring the out condition.");
}
RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", new string[0]);
RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor",
element.Parent?.GetAttributeStringArray("requireddeconstructor", new string[0]) ?? new string[0]);
RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]);
ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty);
InfoText = element.GetAttributeString("infotext", string.Empty);
@@ -915,7 +916,7 @@ namespace Barotrauma
string spriteFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
spriteFolder = Path.GetDirectoryName(filePath);
spriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath);
}
CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
@@ -972,7 +973,7 @@ namespace Barotrauma
string iconFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
iconFolder = Path.GetDirectoryName(filePath);
iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath);
}
UpgradePreviewSprite = new Sprite(subElement, iconFolder, lazyLoad: true);
UpgradePreviewScale = subElement.GetAttributeFloat("scale", 1.0f);
@@ -983,7 +984,7 @@ namespace Barotrauma
string iconFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
iconFolder = Path.GetDirectoryName(filePath);
iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath);
}
InventoryIcon = new Sprite(subElement, iconFolder, lazyLoad: true);
}
@@ -993,7 +994,7 @@ namespace Barotrauma
string iconFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
iconFolder = Path.GetDirectoryName(filePath);
iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath);
}
MinimapIcon = new Sprite(subElement, iconFolder, lazyLoad: true);
}
@@ -1003,7 +1004,7 @@ namespace Barotrauma
string iconFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
iconFolder = Path.GetDirectoryName(filePath);
iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath);
}
InfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true);
@@ -1014,7 +1015,7 @@ namespace Barotrauma
string iconFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
iconFolder = Path.GetDirectoryName(filePath);
iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath);
}
DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true);
@@ -1024,7 +1025,7 @@ namespace Barotrauma
string brokenSpriteFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
brokenSpriteFolder = Path.GetDirectoryName(filePath);
brokenSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath);
}
var brokenSprite = new BrokenItemSprite(
@@ -1034,7 +1035,7 @@ namespace Barotrauma
subElement.GetAttributePoint("offset", Point.Zero));
int spriteIndex = 0;
for (int i = 0; i < BrokenSprites.Count && BrokenSprites[i].MaxCondition < brokenSprite.MaxCondition; i++)
for (int i = 0; i < BrokenSprites.Count && BrokenSprites[i].MaxConditionPercentage < brokenSprite.MaxConditionPercentage; i++)
{
spriteIndex = i;
}
@@ -1044,7 +1045,7 @@ namespace Barotrauma
string decorativeSpriteFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
decorativeSpriteFolder = Path.GetDirectoryName(filePath);
decorativeSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath);
}
int groupID = 0;
@@ -1070,7 +1071,7 @@ namespace Barotrauma
string containedSpriteFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
containedSpriteFolder = Path.GetDirectoryName(filePath);
containedSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath);
}
var containedSprite = new ContainedItemSprite(subElement, containedSpriteFolder, lazyLoad: true);
if (containedSprite.Sprite != null)
@@ -7,7 +7,7 @@ using System.Text;
namespace Barotrauma
{
class Entity : ISpatialEntity
abstract class Entity : ISpatialEntity
{
public const ushort NullEntityID = 0;
public const ushort EntitySpawnerID = ushort.MaxValue;
@@ -16,8 +16,10 @@ namespace Barotrauma
public const ushort ReservedIDStart = ushort.MaxValue - 3;
public const ushort MaxEntityCount = ushort.MaxValue - 2; //ushort.MaxValue - 2 because 0 and ushort.MaxValue are reserved values
private static Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
public static IEnumerable<Entity> GetEntities()
public static IReadOnlyCollection<Entity> GetEntities()
{
return dictionary.Values;
}
@@ -28,11 +30,9 @@ namespace Barotrauma
protected AITarget aiTarget;
private bool idFreed;
public bool Removed { get; private set; }
public virtual bool Removed { get; private set; }
public bool IdFreed => idFreed;
public bool IdFreed { get; private set; }
public readonly ushort ID;
@@ -75,43 +75,65 @@ namespace Barotrauma
this.Submarine = submarine;
spawnTime = Timing.TotalTime;
if (id != NullEntityID && dictionary.ContainsKey(id))
{
throw new Exception($"ID {id} is taken by {dictionary[id]}");
}
//give a unique ID
ID = DetermineID(id, submarine);
if (dictionary.ContainsKey(ID))
{
throw new Exception($"ID {ID} is taken by {dictionary[ID]}");
}
dictionary.Add(ID, this);
}
protected virtual ushort DetermineID(ushort id, Submarine submarine)
{
return id != NullEntityID ?
id :
FindFreeID(submarine == null ? (ushort)1 : submarine.IdOffset);
return id != NullEntityID
? id
: FindFreeId(submarine == null ? (ushort)1 : submarine.IdOffset);
}
public static ushort FindFreeID(ushort idOffset = 0)
private static ushort FindFreeId(ushort idOffset)
{
//ushort.MaxValue - 2 because 0 and ushort.MaxValue are reserved values
if (dictionary.Count >= ushort.MaxValue - 2)
if (dictionary.Count >= MaxEntityCount)
{
throw new Exception("Maximum amount of entities (" + (ushort.MaxValue - 1) + ") reached!");
throw new Exception($"Maximum amount of entities ({MaxEntityCount}) reached!");
}
idOffset = Math.Max(idOffset, (ushort)1);
bool IDfound;
ushort id = idOffset;
do
while (id < ReservedIDStart)
{
id += 1;
IDfound = dictionary.ContainsKey(id);
} while (IDfound || id == NullEntityID || id > ReservedIDStart);
if (!dictionary.ContainsKey(id)) { break; }
id++;
};
return id;
}
/// <summary>
/// Finds a contiguous block of free IDs of at least the given size
/// </summary>
/// <returns>The first ID in the found block, or zero if none are found</returns>
public static int FindFreeIdBlock(int minBlockSize)
{
int currentBlockSize = 0;
for (int i = 1; i < ReservedIDStart; i++)
{
if (dictionary.ContainsKey((ushort)i))
{
currentBlockSize = 0;
}
else
{
currentBlockSize++;
if (currentBlockSize >= minBlockSize)
{
return i - (currentBlockSize-1);
}
}
}
return 0;
}
/// <summary>
/// Find an entity based on the ID
/// </summary>
@@ -134,11 +156,11 @@ namespace Barotrauma
}
catch (Exception exception)
{
DebugConsole.ThrowError("Error while removing entity \"" + e.ToString() + "\"", exception);
DebugConsole.ThrowError($"Error while removing entity \"{e}\"", exception);
GameAnalyticsManager.AddErrorEventOnce(
"Entity.RemoveAll:Exception" + e.ToString(),
$"Entity.RemoveAll:Exception{e}",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Error while removing entity \"" + e.ToString() + " (" + exception.Message + ")\n" + exception.StackTrace.CleanupStackTrace());
$"Error while removing entity \"{e} ({exception.Message})\n{exception.StackTrace.CleanupStackTrace()}");
}
}
StringBuilder errorMsg = new StringBuilder();
@@ -167,7 +189,7 @@ namespace Barotrauma
}
catch (Exception exception)
{
DebugConsole.ThrowError("Error while removing item \"" + item.ToString() + "\"", exception);
DebugConsole.ThrowError($"Error while removing item \"{item}\"", exception);
}
}
Item.ItemList.Clear();
@@ -189,7 +211,7 @@ namespace Barotrauma
}
catch (Exception exception)
{
DebugConsole.ThrowError("Error while removing character \"" + character.ToString() + "\"", exception);
DebugConsole.ThrowError($"Error while removing character \"{character}\"", exception);
}
}
Character.CharacterList.Clear();
@@ -214,35 +236,33 @@ namespace Barotrauma
/// </summary>
public void FreeID()
{
DebugConsole.Log("Removing entity " + ToString() + " (" + ID + ") from entity dictionary.");
if (IdFreed) { return; }
DebugConsole.Log($"Removing entity {ToString()} ({ID}) from entity dictionary.");
if (!dictionary.TryGetValue(ID, out Entity existingEntity))
{
DebugConsole.Log("Entity " + ToString() + " (" + ID + ") not present in entity dictionary.");
DebugConsole.ThrowError($"Entity {ToString()} ({ID}) not present in entity dictionary.");
GameAnalyticsManager.AddErrorEventOnce(
"Entity.FreeID:EntityNotFound" + ID,
$"Entity.FreeID:EntityNotFound{ID}",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Entity " + ToString() + " (" + ID + ") not present in entity dictionary.\n" + Environment.StackTrace.CleanupStackTrace());
$"Entity {ToString()} ({ID}) not present in entity dictionary.\n{Environment.StackTrace.CleanupStackTrace()}");
}
else if (existingEntity != this)
{
DebugConsole.Log("Entity ID mismatch in entity dictionary. Entity " + existingEntity + " had the ID " + ID + " (expecting " + ToString() + ")");
DebugConsole.ThrowError($"Entity ID mismatch in entity dictionary. Entity {existingEntity} had the ID {ID} (expecting {ToString()})");
GameAnalyticsManager.AddErrorEventOnce("Entity.FreeID:EntityMismatch" + ID,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Entity ID mismatch in entity dictionary. Entity " + existingEntity + " had the ID " + ID + " (expecting " + ToString() + ")");
foreach (var keyValuePair in dictionary.Where(kvp => kvp.Value == this).ToList())
{
dictionary.Remove(keyValuePair.Key);
}
$"Entity ID mismatch in entity dictionary. Entity {existingEntity} had the ID {ID} (expecting {ToString()})");
}
dictionary.Remove(ID);
idFreed = true;
else
{
dictionary.Remove(ID);
}
IdFreed = true;
}
public virtual void Remove()
{
if (!idFreed) FreeID();
FreeID();
Removed = true;
}
@@ -255,8 +275,8 @@ namespace Barotrauma
List<string> lines = new List<string>();
for (int i = 0; i < count; i++)
{
lines.Add(entities[i].ID + ": " + entities[i].ToString());
DebugConsole.ThrowError(entities[i].ID + ": " + entities[i].ToString());
lines.Add($"{entities[i].ID}: {entities[i]}");
DebugConsole.ThrowError($"{entities[i].ID}: {entities[i]}");
}
if (!string.IsNullOrWhiteSpace(filename))
@@ -39,6 +39,8 @@ namespace Barotrauma
private readonly float itemRepairStrength;
public readonly HashSet<Submarine> IgnoredSubmarines = new HashSet<Submarine>();
public float EmpStrength { get; set; }
public float BallastFloraDamage { get; set; }
@@ -149,7 +151,7 @@ namespace Barotrauma
if (!MathUtils.NearlyEqual(Attack.GetStructureDamage(1.0f), 0.0f) || !MathUtils.NearlyEqual(Attack.GetLevelWallDamage(1.0f), 0.0f))
{
RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker);
RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, IgnoredSubmarines);
}
if (BallastFloraDamage > 0.0f)
@@ -397,13 +399,14 @@ namespace Barotrauma
/// <summary>
/// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken
/// </summary>
public static Dictionary<Structure, float> RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null)
public static Dictionary<Structure, float> RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable<Submarine> ignoredSubmarines = null)
{
List<Structure> structureList = new List<Structure>();
float dist = 600.0f;
foreach (MapEntity entity in MapEntity.mapEntityList)
{
if (!(entity is Structure structure)) { continue; }
if (ignoredSubmarines != null && entity.Submarine != null && ignoredSubmarines.Contains(entity.Submarine)) { continue; }
if (structure.HasBody &&
!structure.IsPlatform &&
@@ -26,25 +26,25 @@ namespace Barotrauma
}
//a value between 0.0f-1.0f (0.0 = closed, 1.0f = open)
private float open;
private float open;
//the force of the water flow which is exerted on physics bodies
private Vector2 flowForce;
private Hull flowTargetHull;
private float openedTimer = 1.0f;
private float higherSurface;
private float lowerSurface;
private Vector2 lerpedFlowForce;
//if set to true, hull connections of this gap won't be updated when changes are being done to hulls
public bool DisableHullRechecks;
//can ambient light get through the gap even if it's not open
public bool PassAmbientLight;
//a collider outside the gap (for example an ice wall next to the sub)
//used by ragdolls to prevent them from ending up inside colliders when teleporting out of the sub
@@ -54,11 +54,11 @@ namespace Barotrauma
public float Open
{
get { return open; }
set
set
{
if (float.IsNaN(value)) { return; }
if (value > open) { openedTimer = 1.0f; }
open = MathHelper.Clamp(value, 0.0f, 1.0f);
open = MathHelper.Clamp(value, 0.0f, 1.0f);
}
}
@@ -319,7 +319,7 @@ namespace Barotrauma
if (openedTimer > 0.0f && flowForce.Length() > lerpedFlowForce.Length())
{
//if the gap has just been opened/created, allow it to exert a large force instantly without any smoothing
lerpedFlowForce = flowForce;
lerpedFlowForce = flowForce;
}
else
{
@@ -375,7 +375,7 @@ namespace Barotrauma
//make sure not to move more than what the room contains
delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 5.0f * sizeModifier, Math.Min(hull2.WaterVolume, hull2.Volume));
//make sure not to place more water to the target room than it can hold
delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - (hull1.WaterVolume));
hull1.WaterVolume += delta;
@@ -405,7 +405,7 @@ namespace Barotrauma
{
hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure-subOffset.Y) + hull2.Pressure) / 2);
}
flowForce = new Vector2(delta, 0.0f);
}
@@ -448,7 +448,7 @@ namespace Barotrauma
hull2.WaterVolume -= delta;
flowForce = new Vector2(
0.0f,
0.0f,
Math.Min(Math.Min((hull2.Pressure + subOffset.Y) - hull1.Pressure, 200.0f), delta));
flowTargetHull = hull1;
@@ -456,7 +456,7 @@ namespace Barotrauma
if (hull1.WaterVolume > hull1.Volume)
{
hull1.Pressure = Math.Max(hull1.Pressure, (hull1.Pressure + (hull2.Pressure + subOffset.Y)) / 2);
}
}
}
//there's water in the upper room, drop to lower
@@ -494,7 +494,7 @@ namespace Barotrauma
hull1.LethalPressure = avgLethality;
hull2.LethalPressure = avgLethality;
}
else
else
{
hull1.LethalPressure = 0.0f;
hull2.LethalPressure = 0.0f;
@@ -511,7 +511,7 @@ namespace Barotrauma
float sizeModifier = size * open * open;
float delta = 500.0f * sizeModifier * deltaTime;
//make sure not to place more water to the target room than it can hold
delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume);
hull1.WaterVolume += delta;
@@ -526,7 +526,7 @@ namespace Barotrauma
if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f)
{
flowForce = new Vector2(-delta, 0.0f);
}
else
{
@@ -554,7 +554,7 @@ namespace Barotrauma
hull1.WaveVel[0] += vel;
hull1.WaveVel[1] += vel;
}
}
}
else
{
@@ -651,12 +651,12 @@ namespace Barotrauma
float totalOxygen = hull1.Oxygen + hull2.Oxygen;
float totalVolume = (hull1.Volume + hull2.Volume);
float deltaOxygen = (totalOxygen * hull1.Volume / totalVolume) - hull1.Oxygen;
deltaOxygen = MathHelper.Clamp(deltaOxygen, -Hull.OxygenDistributionSpeed, Hull.OxygenDistributionSpeed);
hull1.Oxygen += deltaOxygen;
hull2.Oxygen -= deltaOxygen;
hull2.Oxygen -= deltaOxygen;
}
public static Gap FindAdjacent(IEnumerable<Gap> gaps, Vector2 worldPos, float allowedOrthogonalDist)
@@ -724,7 +724,7 @@ namespace Barotrauma
{
if (!DisableHullRechecks) FindHulls();
}
public static Gap Load(XElement element, Submarine submarine, IdRemap idRemap)
{
Rectangle rect = Rectangle.Empty;
@@ -755,6 +755,8 @@ namespace Barotrauma
{
linkedToID = new List<ushort>(),
};
g.HiddenInGame = element.GetAttributeBool(nameof(HiddenInGame).ToLower(), g.HiddenInGame);
return g;
}
@@ -764,7 +766,8 @@ namespace Barotrauma
element.Add(
new XAttribute("ID", ID),
new XAttribute("horizontal", IsHorizontal ? "true" : "false"));
new XAttribute("horizontal", IsHorizontal ? "true" : "false"),
new XAttribute(nameof(HiddenInGame).ToLower(), HiddenInGame));
element.Add(new XAttribute("rect",
(int)(rect.X - Submarine.HiddenSubPosition.X) + "," +
@@ -1058,11 +1058,17 @@ namespace Barotrauma
/// <param name="inclusive">Does being exactly at the edge of the hull count as being inside?</param>
public static Hull FindHull(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true)
{
if (EntityGrids == null) return null;
if (EntityGrids == null)
{
return null;
}
if (guess != null)
{
if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess;
if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive))
{
return guess;
}
}
foreach (EntityGrid entityGrid in EntityGrids)
@@ -1088,15 +1094,19 @@ namespace Barotrauma
continue;
}
}
Vector2 transformedPosition = position;
if (useWorldCoordinates && entityGrid.Submarine != null) transformedPosition -= entityGrid.Submarine.Position;
if (useWorldCoordinates && entityGrid.Submarine != null)
{
transformedPosition -= entityGrid.Submarine.Position;
}
var entities = entityGrid.GetEntities(transformedPosition);
if (entities == null) continue;
if (entities == null) { continue; }
foreach (Hull hull in entities)
{
if (Submarine.RectContains(hull.rect, transformedPosition, inclusive)) return hull;
if (Submarine.RectContains(hull.rect, transformedPosition, inclusive))
{
return hull;
}
}
}
@@ -145,8 +145,7 @@ namespace Barotrauma
public static List<MapEntity> PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false)
{
int idOffset = Entity.FindFreeID(1);
if (MapEntity.mapEntityList.Any()) { idOffset = MapEntity.mapEntityList.Max(e => e.ID); }
int idOffset = Entity.FindFreeIdBlock(configElement.Elements().Count());
List<MapEntity> entities = MapEntity.LoadAll(sub, configElement, filePath, idOffset);
if (entities.Count == 0) { return entities; }
@@ -202,10 +202,10 @@ namespace Barotrauma
get { return startPosition.ToVector2(); }
}
private Vector2 startExitPosition;
private Point startExitPosition;
public Vector2 StartExitPosition
{
get { return startExitPosition; }
get { return startExitPosition.ToVector2(); }
}
public Point Size
@@ -218,10 +218,10 @@ namespace Barotrauma
get { return endPosition.ToVector2(); }
}
private Vector2 endExitPosition;
private Point endExitPosition;
public Vector2 EndExitPosition
{
get { return endExitPosition; }
get { return endExitPosition.ToVector2(); }
}
public int BottomPos
@@ -366,9 +366,6 @@ namespace Barotrauma
{
this.LevelData = levelData;
borders = new Rectangle(Point.Zero, levelData.Size);
//remove from entity dictionary
//base.Remove();
}
public static Level Generate(LevelData levelData, bool mirror, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
@@ -462,12 +459,12 @@ namespace Barotrauma
startPosition = new Point(
(int)MathHelper.Lerp(minMainPathWidth, borders.Width - minMainPathWidth, GenerationParams.StartPosition.X),
(int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.StartPosition.Y));
startExitPosition = new Vector2(startPosition.X, borders.Bottom);
startExitPosition = new Point(startPosition.X, borders.Bottom);
endPosition = new Point(
(int)MathHelper.Lerp(minMainPathWidth, borders.Width - minMainPathWidth, GenerationParams.EndPosition.X),
(int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y));
endExitPosition = new Vector2(endPosition.X, borders.Bottom);
endExitPosition = new Point(endPosition.X, borders.Bottom);
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
@@ -486,25 +483,25 @@ namespace Barotrauma
{
startPath = new Tunnel(
TunnelType.SidePath,
new List<Point>() { startExitPosition.ToPoint(), startPosition },
new List<Point>() { startExitPosition, startPosition },
minWidth / 2, parentTunnel: mainPath);
Tunnels.Add(startPath);
}
else
{
startExitPosition = StartPosition;
startExitPosition = startPosition;
}
if (GenerationParams.EndPosition.Y < 0.5f && (Mirrored ? !HasStartOutpost() : !HasEndOutpost()))
{
endPath = new Tunnel(
TunnelType.SidePath,
new List<Point>() { endPosition, endExitPosition.ToPoint() },
new List<Point>() { endPosition, endExitPosition },
minWidth / 2, parentTunnel: mainPath);
Tunnels.Add(endPath);
}
else
{
endExitPosition = EndPosition;
endExitPosition = endPosition;
}
if (GenerationParams.CreateHoleNextToEnd)
@@ -513,14 +510,14 @@ namespace Barotrauma
{
endHole = new Tunnel(
TunnelType.SidePath,
new List<Point>() { startPosition, startExitPosition.ToPoint(), new Point(0, Size.Y) },
new List<Point>() { startPosition, startExitPosition, new Point(0, Size.Y) },
minWidth / 2, parentTunnel: mainPath);
}
else
{
endHole = new Tunnel(
TunnelType.SidePath,
new List<Point>() { endPosition, endExitPosition.ToPoint(), Size },
new List<Point>() { endPosition, endExitPosition, Size },
minWidth / 2, parentTunnel: mainPath);
}
Tunnels.Add(endHole);
@@ -799,8 +796,12 @@ namespace Barotrauma
for (int i = 0; i < GenerationParams.RuinCount; i++)
{
Point ruinSize = new Point(5000);
ruinPositions.Add(FindPosAwayFromMainPath((Math.Max(ruinSize.X, ruinSize.Y) + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true,
limits: new Rectangle(new Point(ruinSize.X / 2, ruinSize.Y / 2), Size - ruinSize)));
int limitLeft = Math.Max(startPosition.X, ruinSize.X / 2);
int limitRight = Math.Min(endPosition.X, Size.X - ruinSize.X / 2);
Rectangle limits = new Rectangle(limitLeft, ruinSize.Y, limitRight - limitLeft, Size.Y - ruinSize.Y);
Debug.Assert(limits.Width > 0);
Debug.Assert(limits.Height > 0);
ruinPositions.Add(FindPosAwayFromMainPath((Math.Max(ruinSize.X, ruinSize.Y) + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true, limits: limits));
CalculateTunnelDistanceField(ruinPositions);
}
@@ -1004,7 +1005,7 @@ namespace Barotrauma
{
if (pos.PositionType != PositionType.MainPath && pos.PositionType != PositionType.SidePath) { continue; }
if (pos.Position.X < 5000 || pos.Position.X > Size.X - 5000) { continue; }
if (Math.Abs(pos.Position.X - StartPosition.X) < minMainPathWidth * 2 || Math.Abs(pos.Position.X - EndPosition.X) < minMainPathWidth * 2) { continue; }
if (Math.Abs(pos.Position.X - startPosition.X) < minMainPathWidth * 2 || Math.Abs(pos.Position.X - endPosition.X) < minMainPathWidth * 2) { continue; }
if (GetTooCloseCells(pos.Position.ToVector2(), minMainPathWidth * 0.7f).Count > 0) { continue; }
iceChunkPositions.Add(pos.Position);
}
@@ -1187,19 +1188,19 @@ namespace Barotrauma
startPosition = endPosition;
endPosition = tempP;
Vector2 tempV = startExitPosition;
tempP = startExitPosition;
startExitPosition = endExitPosition;
endExitPosition = tempV;
endExitPosition = tempP;
}
if (StartOutpost != null)
{
startExitPosition = StartOutpost.WorldPosition;
startPosition = startExitPosition.ToPoint();
startExitPosition = StartOutpost.WorldPosition.ToPoint();
startPosition = startExitPosition;
}
if (EndOutpost != null)
{
endExitPosition = EndOutpost.WorldPosition;
endPosition = endExitPosition.ToPoint();
endExitPosition = EndOutpost.WorldPosition.ToPoint();
endPosition = endExitPosition;
}
CreateWrecks();
@@ -2321,7 +2322,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 (!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 _);
@@ -2738,10 +2739,26 @@ namespace Barotrauma
allValidLocations.Sort((x, y) => Vector2.DistanceSquared(poiPos, x.EdgeCenter)
.CompareTo(Vector2.DistanceSquared(poiPos, y.EdgeCenter)));
var maxResourceOverlap = 0.4f;
// TODO: Find multiple locations if there's too many resources to fit on a sigle edge
var selectedLocation = allValidLocations.FirstOrDefault(l =>
Vector2.Distance(l.Edge.Point1, l.Edge.Point2) is float edgeLength &&
requiredAmount <= (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * prefab.Size.X)));
if (selectedLocation.Edge == null)
{
//couldn't find a long enough edge, find the largest one
float longestEdge = 0.0f;
foreach (var validLocation in allValidLocations)
{
if (Vector2.Distance(validLocation.Edge.Point1, validLocation.Edge.Point2) is float edgeLength && edgeLength > longestEdge)
{
selectedLocation = validLocation;
longestEdge = edgeLength;
}
}
}
if (selectedLocation.Edge == null)
{
throw new Exception("Failed to find a suitable level wall edge to place level resources on.");
}
PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources);
var edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell);
rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
@@ -3197,12 +3214,12 @@ namespace Barotrauma
public bool IsCloseToStart(Point position, float minDist)
{
return MathUtils.LineSegmentToPointDistanceSquared(StartPosition.ToPoint(), StartExitPosition.ToPoint(), position) < minDist * minDist;
return MathUtils.LineSegmentToPointDistanceSquared(startPosition, startExitPosition, position) < minDist * minDist;
}
public bool IsCloseToEnd(Point position, float minDist)
{
return MathUtils.LineSegmentToPointDistanceSquared(EndPosition.ToPoint(), EndExitPosition.ToPoint(), position) < minDist * minDist;
return MathUtils.LineSegmentToPointDistanceSquared(endPosition, endExitPosition, position) < minDist * minDist;
}
private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type)
@@ -3214,6 +3231,7 @@ namespace Barotrauma
var waypoints = WayPoint.WayPointList.Where(wp =>
wp.Submarine == null &&
wp.SpawnType == SpawnType.Path &&
wp.WorldPosition.X < EndExitPosition.X &&
!IsCloseToStart(wp.WorldPosition, minDistance) &&
!IsCloseToEnd(wp.WorldPosition, minDistance)).ToList();
@@ -3971,7 +3989,7 @@ namespace Barotrauma
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, randSync: Rand.RandSync.Server);
var corpse = Character.Create(CharacterPrefab.HumanConfigFile, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
corpse.AnimController.FindHull(worldPos, true);
corpse.AnimController.FindHull(worldPos, setSubmarine: true);
corpse.TeamID = CharacterTeamType.None;
corpse.EnableDespawn = false;
selectedPrefab.GiveItems(corpse, wreck);
@@ -143,14 +143,20 @@ namespace Barotrauma
var rand = new MTRandom(ToolBox.StringToInt(Seed));
InitialDepth = (int)MathHelper.Lerp(GenerationParams.InitialDepthMin, GenerationParams.InitialDepthMax, (float)rand.NextDouble());
//minimum difficulty of the level before hunting grounds can appear
float huntingGroundsDifficultyThreshold = 25;
//probability of hunting grounds appearing in 100% difficulty levels
float maxHuntingGroundsProbability = 0.3f;
HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability;
HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max();
if (Biome.IsEndBiome)
{
HasHuntingGrounds = false;
HasBeaconStation = false;
}
else
{
//minimum difficulty of the level before hunting grounds can appear
float huntingGroundsDifficultyThreshold = 25;
//probability of hunting grounds appearing in 100% difficulty levels
float maxHuntingGroundsProbability = 0.3f;
HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability;
HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max();
}
IsBeaconActive = false;
}

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