Build 0.21.6.0 (1.0 pre-patch)
This commit is contained in:
@@ -206,13 +206,19 @@ namespace Barotrauma
|
||||
private set;
|
||||
} = new HashSet<Submarine>();
|
||||
|
||||
public bool IsTargetingPlayerTeam => IsTargetInPlayerTeam(SelectedAiTarget);
|
||||
public static bool IsTargetBeingChasedBy(Character target, Character character)
|
||||
=> character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && (enemyAI.State == AIState.Attack || enemyAI.State == AIState.Aggressive);
|
||||
public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c);
|
||||
private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character);
|
||||
|
||||
private bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam;
|
||||
private static bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam;
|
||||
|
||||
private bool IsAttackingOwner(Character other) =>
|
||||
PetBehavior != null && PetBehavior.Owner != null &&
|
||||
!other.IsUnconscious && !other.IsArrested &&
|
||||
other.AIController is HumanAIController humanAI &&
|
||||
humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat &&
|
||||
combat.Enemy != null && combat.Enemy == PetBehavior.Owner;
|
||||
|
||||
private bool reverse;
|
||||
public bool Reverse
|
||||
@@ -355,6 +361,10 @@ namespace Barotrauma
|
||||
{
|
||||
targetingTag = "owner";
|
||||
}
|
||||
else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter)))
|
||||
{
|
||||
targetingTag = "hostile";
|
||||
}
|
||||
else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP))
|
||||
{
|
||||
targetingTag = tP.Tag;
|
||||
@@ -365,7 +375,7 @@ namespace Barotrauma
|
||||
{
|
||||
targetingTag = "husk";
|
||||
}
|
||||
else if (!Character.IsFriendly(targetCharacter))
|
||||
else if (!Character.IsSameSpeciesOrGroup(targetCharacter))
|
||||
{
|
||||
if (enemy.CombatStrength > CombatStrength)
|
||||
{
|
||||
@@ -677,22 +687,22 @@ namespace Barotrauma
|
||||
{
|
||||
if (SelectedAiTarget.Entity is Character targetCharacter)
|
||||
{
|
||||
bool IsValid(Character.Attacker a)
|
||||
bool ShouldRetaliate(Character.Attacker a)
|
||||
{
|
||||
Character c = a.Character;
|
||||
if (c.IsDead || c.Removed) { return false; }
|
||||
if (!Character.IsFriendly(c)) { return true; }
|
||||
if (!c.IsPlayer) { return false; }
|
||||
// Only apply the threshold to players
|
||||
return a.Damage >= selectedTargetingParams.Threshold;
|
||||
if (c == null || c.IsUnconscious || c.Removed) { return false; }
|
||||
// Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group.
|
||||
if (Character.IsSameSpeciesOrGroup(c)) { return false; }
|
||||
if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; }
|
||||
if (c.IsPlayer || Character.IsOnFriendlyTeam(c))
|
||||
{
|
||||
return a.Damage >= selectedTargetingParams.Threshold;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character;
|
||||
//if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState
|
||||
//otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it
|
||||
//TODO: a way for pets to differentiate hostile and friendly humans?
|
||||
if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget) && !attacker.IsFriendly(targetCharacter))
|
||||
Character attacker = targetCharacter.LastAttackers.LastOrDefault(ShouldRetaliate)?.Character;
|
||||
if (attacker?.AiTarget != null)
|
||||
{
|
||||
// Attack the character that attacked the target we are protecting
|
||||
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
|
||||
SelectTarget(attacker.AiTarget);
|
||||
State = AIState.Attack;
|
||||
@@ -1501,7 +1511,7 @@ namespace Barotrauma
|
||||
{
|
||||
hitTarget = limb.character;
|
||||
}
|
||||
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget))
|
||||
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget) && !IsAttackingOwner(hitTarget))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -2315,7 +2325,7 @@ namespace Barotrauma
|
||||
{
|
||||
t = limb.character;
|
||||
}
|
||||
if (t != null && (t == target || !Character.IsFriendly(t)))
|
||||
if (t != null && (t == target || (!Character.IsFriendly(t) || IsAttackingOwner(t))))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ namespace Barotrauma
|
||||
UseIndoorSteeringOutside = false;
|
||||
}
|
||||
|
||||
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID))
|
||||
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
|
||||
{
|
||||
// Spot enemies while staying outside or inside an enemy ship.
|
||||
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
|
||||
@@ -541,7 +541,7 @@ 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 && Character.NeedsOxygen;
|
||||
bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, AIObjectiveFindDivingGear.OXYGEN_SOURCE, out _, conditionPercentage: 1);
|
||||
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
|
||||
|
||||
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
|
||||
@@ -566,17 +566,17 @@ namespace Barotrauma
|
||||
gotoObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
if (!oxygenLow)
|
||||
if (!shouldActOnSuffocation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Diving gear
|
||||
if (oxygenLow || findItemState != FindItemState.OtherItem)
|
||||
if (shouldActOnSuffocation || findItemState != FindItemState.OtherItem)
|
||||
{
|
||||
bool needsGear = NeedsDivingGear(Character.CurrentHull, out _);
|
||||
if (!needsGear || oxygenLow)
|
||||
if (!needsGear || shouldActOnSuffocation)
|
||||
{
|
||||
bool isCurrentObjectiveFindSafety = ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>();
|
||||
bool shouldKeepTheGearOn =
|
||||
@@ -591,14 +591,14 @@ namespace Barotrauma
|
||||
Character.CurrentHull.IsWetRoom;
|
||||
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
|
||||
bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
|
||||
if (oxygenLow && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
|
||||
if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
|
||||
{
|
||||
shouldKeepTheGearOn = false;
|
||||
// Remove the suit before we pass out
|
||||
removeDivingSuit = true;
|
||||
}
|
||||
bool takeMaskOff = !shouldKeepTheGearOn;
|
||||
if (!shouldKeepTheGearOn && !oxygenLow)
|
||||
if (!shouldKeepTheGearOn && !shouldActOnSuffocation)
|
||||
{
|
||||
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
|
||||
{
|
||||
@@ -647,7 +647,7 @@ namespace Barotrauma
|
||||
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
|
||||
if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS))
|
||||
{
|
||||
if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
{
|
||||
divingSuit.Drop(Character);
|
||||
HandleRelocation(divingSuit);
|
||||
@@ -982,7 +982,7 @@ namespace Barotrauma
|
||||
if (target.CurrentHull != hull) { continue; }
|
||||
if (AIObjectiveRescueAll.IsValidTarget(target, Character))
|
||||
{
|
||||
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
|
||||
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
@@ -1161,7 +1161,7 @@ namespace Barotrauma
|
||||
freezeAI = true;
|
||||
}
|
||||
}
|
||||
if (attacker == null || attacker.IsDead || attacker.Removed)
|
||||
if (attacker == null || attacker.IsUnconscious || attacker.Removed)
|
||||
{
|
||||
// Don't react to the damage if there's no attacker.
|
||||
// We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying.
|
||||
@@ -1199,7 +1199,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker);
|
||||
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null;
|
||||
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null;
|
||||
if (isAccidental)
|
||||
{
|
||||
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)
|
||||
@@ -1279,7 +1279,7 @@ namespace Barotrauma
|
||||
if (otherCharacter.Submarine != attacker.Submarine) { continue; }
|
||||
if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; }
|
||||
if (otherCharacter.IsPlayer) { continue; }
|
||||
if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; }
|
||||
if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; }
|
||||
if (!otherHumanAI.IsFriendly(Character)) { continue; }
|
||||
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
|
||||
if (!isWitnessing)
|
||||
@@ -1299,7 +1299,7 @@ namespace Barotrauma
|
||||
|
||||
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false)
|
||||
{
|
||||
if (!(c.AIController is HumanAIController humanAI)) { return AIObjectiveCombat.CombatMode.None; }
|
||||
if (c.AIController is not HumanAIController humanAI) { return AIObjectiveCombat.CombatMode.None; }
|
||||
if (!IsFriendly(attacker))
|
||||
{
|
||||
if (c.Submarine == null)
|
||||
@@ -1327,7 +1327,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (attacker.IsPlayer && c.TeamID == attacker.TeamID)
|
||||
{
|
||||
if (GameMain.IsSingleplayer || Character.TeamID != attacker.TeamID)
|
||||
if (GameMain.IsSingleplayer || c.TeamID != attacker.TeamID)
|
||||
{
|
||||
// Bots in the player team never act aggressively in single player when attacked by the player
|
||||
// In multiplayer, they react only to players attacking them or other crew members
|
||||
@@ -1345,11 +1345,11 @@ namespace Barotrauma
|
||||
isAttackerFightingEnemy = true;
|
||||
return AIObjectiveCombat.CombatMode.None;
|
||||
}
|
||||
if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
|
||||
if (isWitnessing && c.CombatAction != null && !c.IsSecurity)
|
||||
{
|
||||
return Character.CombatAction.WitnessReaction;
|
||||
return c.CombatAction.WitnessReaction;
|
||||
}
|
||||
if (attacker.IsPlayer && FindInstigator() is Character instigator)
|
||||
if (!attacker.IsInstigator && c.IsOnFriendlyTeam(attacker) && FindInstigator() is Character instigator)
|
||||
{
|
||||
// The guards don't react to player's aggressions when there's an instigator around
|
||||
isAttackerFightingEnemy = true;
|
||||
@@ -1359,11 +1359,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (c.IsSecurity)
|
||||
{
|
||||
return Character.CombatAction != null ? Character.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.None;
|
||||
return attacker.CombatAction != null ? attacker.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.Offensive;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.None;
|
||||
return attacker.CombatAction != null ? attacker.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1514,9 +1514,18 @@ namespace Barotrauma
|
||||
startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f);
|
||||
|
||||
//do a raycast upwards to find any walls
|
||||
float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f;
|
||||
if (!Character.AnimController.TryGetCollider(0, out PhysicsBody mainCollider))
|
||||
{
|
||||
mainCollider = Character.AnimController.Collider;
|
||||
}
|
||||
float margin = 0.1f;
|
||||
if (shouldCrouch)
|
||||
{
|
||||
margin *= 2;
|
||||
}
|
||||
float minCeilingDist = mainCollider.height / 2 + mainCollider.radius + margin;
|
||||
|
||||
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return !(fixture.Body.UserData is Submarine); }) != null;
|
||||
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null;
|
||||
}
|
||||
|
||||
public bool AllowCampaignInteraction()
|
||||
@@ -1558,20 +1567,20 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage);
|
||||
public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the character has a diving suit in usable condition plus some oxygen.
|
||||
/// </summary>
|
||||
public static bool HasDivingSuit(Character character, float conditionPercentage = 0)
|
||||
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true,
|
||||
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
|
||||
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true,
|
||||
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes));
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the character has a diving mask in usable condition plus some oxygen.
|
||||
/// </summary>
|
||||
public static bool HasDivingMask(Character character, float conditionPercentage = 0)
|
||||
=> HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
|
||||
public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
|
||||
=> HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true);
|
||||
|
||||
private static List<Item> matchingItems = new List<Item>();
|
||||
|
||||
@@ -1589,7 +1598,27 @@ namespace Barotrauma
|
||||
(!requireEquipped || character.HasEquippedItem(i)) &&
|
||||
(predicate == null || predicate(i)), recursive, matchingItems);
|
||||
items = matchingItems;
|
||||
return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.OwnInventory == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)));
|
||||
foreach (var item in matchingItems)
|
||||
{
|
||||
if (item == null) { continue; }
|
||||
|
||||
if (containedTag.IsEmpty || item.OwnInventory == null)
|
||||
{
|
||||
//no contained items required, this item's ok
|
||||
return true;
|
||||
}
|
||||
var suitableSlot = item.GetComponent<ItemContainer>().FindSuitableSubContainerIndex(containedTag);
|
||||
if (suitableSlot == null)
|
||||
{
|
||||
//no restrictions on the suitable slot
|
||||
return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage);
|
||||
}
|
||||
else
|
||||
{
|
||||
return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage && it.ParentInventory.IsInSlot(it, suitableSlot.Value));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void StructureDamaged(Structure structure, float damageAmount, Character character)
|
||||
@@ -2016,11 +2045,9 @@ namespace Barotrauma
|
||||
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
|
||||
{
|
||||
bool sameTeam = me.TeamID == other.TeamID;
|
||||
bool friendlyTeam = IsOnFriendlyTeam(me, other);
|
||||
bool teamGood = sameTeam || friendlyTeam && !onlySameTeam;
|
||||
bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other);
|
||||
if (!teamGood) { return false; }
|
||||
bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
|
||||
if (!speciesGood) { return false; }
|
||||
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
|
||||
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
var reputation = campaign.Map?.CurrentLocation?.Reputation;
|
||||
@@ -2029,30 +2056,14 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet)
|
||||
{
|
||||
// Hostile NPCs are hostile to all pets, unless they are in the same team.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
|
||||
{
|
||||
if (myTeam == otherTeam) { return true; }
|
||||
|
||||
switch (myTeam)
|
||||
{
|
||||
case CharacterTeamType.None:
|
||||
case CharacterTeamType.Team1:
|
||||
case CharacterTeamType.Team2:
|
||||
// Only friendly to the same team and friendly NPCs
|
||||
return otherTeam == CharacterTeamType.FriendlyNPC;
|
||||
case CharacterTeamType.FriendlyNPC:
|
||||
// Friendly NPCs are friendly to both teams
|
||||
return otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
|
||||
|
||||
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
|
||||
|
||||
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
|
||||
|
||||
+12
-4
@@ -352,7 +352,7 @@ namespace Barotrauma
|
||||
Weapon = null;
|
||||
continue;
|
||||
}
|
||||
if (WeaponComponent.IsLoaded(character))
|
||||
if (WeaponComponent.IsNotEmpty(character))
|
||||
{
|
||||
// All good, the weapon is loaded
|
||||
break;
|
||||
@@ -470,7 +470,7 @@ namespace Barotrauma
|
||||
// Not in the inventory anymore or cannot find the weapon component
|
||||
return false;
|
||||
}
|
||||
if (!WeaponComponent.IsLoaded(character))
|
||||
if (!WeaponComponent.IsNotEmpty(character))
|
||||
{
|
||||
// Try reloading (and seek ammo)
|
||||
if (!Reload(seekAmmo))
|
||||
@@ -541,7 +541,7 @@ namespace Barotrauma
|
||||
priority /= 2;
|
||||
}
|
||||
}
|
||||
if (!weapon.IsLoaded(character))
|
||||
if (!weapon.IsNotEmpty(character))
|
||||
{
|
||||
if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
|
||||
{
|
||||
@@ -554,7 +554,15 @@ namespace Barotrauma
|
||||
priority /= 2;
|
||||
}
|
||||
}
|
||||
if (Enemy.IsKnockedDown)
|
||||
|
||||
if (Enemy.Params.Health.StunImmunity)
|
||||
{
|
||||
if (weapon.Item.HasTag("stunner"))
|
||||
{
|
||||
priority /= 2;
|
||||
}
|
||||
}
|
||||
else if (Enemy.IsKnockedDown)
|
||||
{
|
||||
// Enemy is stunned, reduce the priority of stunner weapons.
|
||||
Attack attack = GetAttackDefinition(weapon);
|
||||
|
||||
+3
-2
@@ -98,7 +98,7 @@ namespace Barotrauma
|
||||
int containedItemCount = 0;
|
||||
foreach (Item it in container.Inventory.AllItems)
|
||||
{
|
||||
if (CheckItem(it))
|
||||
if (CheckItem(it) && IsInTargetSlot(it))
|
||||
{
|
||||
containedItemCount++;
|
||||
}
|
||||
@@ -244,7 +244,8 @@ namespace Barotrauma
|
||||
|
||||
public bool IsInTargetSlot(Item item)
|
||||
{
|
||||
if (container?.Inventory is ItemInventory inventory && TargetSlot is not null)
|
||||
if (TargetSlot == null) { return true; }
|
||||
if (container?.Inventory is ItemInventory inventory)
|
||||
{
|
||||
return inventory.IsInSlot(item, (int)TargetSlot);
|
||||
}
|
||||
|
||||
+37
-8
@@ -1,5 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Barotrauma
|
||||
private AIObjectiveGetItem getDivingGear;
|
||||
private AIObjectiveContainItem getOxygen;
|
||||
private Item targetItem;
|
||||
private int? oxygenSourceSlotIndex;
|
||||
|
||||
public const float MIN_OXYGEN = 10;
|
||||
|
||||
@@ -43,12 +44,15 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
targetItem = character.Inventory.FindItemByTag(gearTag, true);
|
||||
|
||||
TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true));
|
||||
if (targetItem == null && gearTag == LIGHT_DIVING_GEAR)
|
||||
{
|
||||
targetItem = character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true);
|
||||
TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true));
|
||||
}
|
||||
if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
|
||||
if (targetItem == null ||
|
||||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) &&
|
||||
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
|
||||
{
|
||||
TryAddSubObjective(ref getDivingGear, () =>
|
||||
{
|
||||
@@ -84,7 +88,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
float min = GetMinOxygen(character);
|
||||
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min))
|
||||
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => IsSuitableContainedOxygenSource(it)))
|
||||
{
|
||||
TryAddSubObjective(ref getOxygen, () =>
|
||||
{
|
||||
@@ -93,7 +97,7 @@ namespace Barotrauma
|
||||
if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min))
|
||||
{
|
||||
character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f);
|
||||
if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1)
|
||||
if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min, recursive: true).Count == 1)
|
||||
{
|
||||
character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f);
|
||||
}
|
||||
@@ -109,7 +113,8 @@ namespace Barotrauma
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
ConditionLevel = MIN_OXYGEN,
|
||||
RemoveExistingWhenNecessary = true
|
||||
RemoveExistingWhenNecessary = true,
|
||||
TargetSlot = oxygenSourceSlotIndex
|
||||
};
|
||||
if (container.HasSubContainers)
|
||||
{
|
||||
@@ -167,12 +172,36 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsSuitableContainedOxygenSource(Item item)
|
||||
{
|
||||
return
|
||||
item != null &&
|
||||
item.HasTag(OXYGEN_SOURCE) &&
|
||||
item.Condition > 0 &&
|
||||
(oxygenSourceSlotIndex == null || item.ParentInventory.IsInSlot(item, oxygenSourceSlotIndex.Value));
|
||||
}
|
||||
|
||||
private void TrySetTargetItem(Item item)
|
||||
{
|
||||
if (targetItem == item) { return; }
|
||||
targetItem = item;
|
||||
if (targetItem != null)
|
||||
{
|
||||
oxygenSourceSlotIndex = targetItem.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(OXYGEN_SOURCE);
|
||||
}
|
||||
else
|
||||
{
|
||||
oxygenSourceSlotIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
getDivingGear = null;
|
||||
getOxygen = null;
|
||||
targetItem = null;
|
||||
oxygenSourceSlotIndex = null;
|
||||
}
|
||||
|
||||
public static float GetMinOxygen(Character character)
|
||||
|
||||
+8
-5
@@ -63,15 +63,16 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
|
||||
if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
|
||||
(HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
|
||||
(needsSuit ?
|
||||
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) :
|
||||
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character))))
|
||||
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))))
|
||||
{
|
||||
Priority = 100;
|
||||
}
|
||||
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
|
||||
character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
|
||||
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
|
||||
{
|
||||
// Ordered to follow, hold position, or return back to main sub inside a hostile sub
|
||||
// -> ignore find safety unless we need to find a diving gear
|
||||
@@ -137,12 +138,14 @@ namespace Barotrauma
|
||||
private float retryTimer;
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (resetPriority) { return; }
|
||||
var currentHull = character.CurrentHull;
|
||||
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
|
||||
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0;
|
||||
if (!character.LockHands && (!dangerousPressure || cannotFindSafeHull))
|
||||
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
|
||||
{
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
|
||||
bool needsEquipment = false;
|
||||
bool needsEquipment = shouldActOnSuffocation;
|
||||
if (needsDivingSuit)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.GetMinOxygen(character));
|
||||
|
||||
+9
-1
@@ -178,7 +178,7 @@ namespace Barotrauma
|
||||
requiredCondition = () =>
|
||||
Leak.Submarine == character.Submarine &&
|
||||
Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))),
|
||||
endNodeFilter = n => n.Waypoint.CurrentHull != null && Leak.linkedTo.Any(e => e is Hull h && h == n.Waypoint.CurrentHull),
|
||||
endNodeFilter = IsSuitableEndNode,
|
||||
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
|
||||
SpeakCannotReachCondition = () => !CheckObjectiveSpecific()
|
||||
},
|
||||
@@ -197,6 +197,14 @@ namespace Barotrauma
|
||||
}
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref gotoObjective));
|
||||
|
||||
bool IsSuitableEndNode(PathNode n)
|
||||
{
|
||||
if (n.Waypoint.CurrentHull is null) { return false; }
|
||||
if (n.Waypoint.CurrentHull.ConnectedGaps.Contains(Leak)) { return true; }
|
||||
// Accept also nodes located in the linked hulls (multi-hull rooms)
|
||||
return Leak.linkedTo.Any(e => e is Hull h && h.linkedTo.Contains(n.Waypoint.CurrentHull));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -54,7 +54,7 @@ namespace Barotrauma
|
||||
public bool AllowVariants { get; set; }
|
||||
public bool Equip { get; set; }
|
||||
public bool Wear { get; set; }
|
||||
public bool RequireLoaded { get; set; }
|
||||
public bool RequireNonEmpty { get; set; }
|
||||
public bool EvaluateCombatPriority { get; set; }
|
||||
public bool CheckPathForEachItem { get; set; }
|
||||
public bool SpeakIfFails { get; set; }
|
||||
@@ -391,10 +391,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; }
|
||||
}
|
||||
float itemPriority = 1;
|
||||
float itemPriority = item.Prefab.BotPriority;
|
||||
if (GetItemPriority != null)
|
||||
{
|
||||
itemPriority = GetItemPriority(item);
|
||||
itemPriority *= GetItemPriority(item);
|
||||
}
|
||||
Entity rootInventoryOwner = item.GetRootInventoryOwner();
|
||||
if (rootInventoryOwner is Item ownerItem)
|
||||
@@ -513,7 +513,7 @@ namespace Barotrauma
|
||||
float lowestCost = float.MaxValue;
|
||||
foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
|
||||
{
|
||||
if (!(prefab is ItemPrefab itemPrefab)) { continue; }
|
||||
if (prefab is not ItemPrefab itemPrefab) { continue; }
|
||||
if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id)))
|
||||
{
|
||||
float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ?
|
||||
@@ -561,7 +561,7 @@ namespace Barotrauma
|
||||
if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { 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; }
|
||||
if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; }
|
||||
return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ namespace Barotrauma
|
||||
public bool CheckInventory { get; set; }
|
||||
public bool EvaluateCombatPriority { get; set; }
|
||||
public bool CheckPathForEachItem { get; set; }
|
||||
public bool RequireLoaded { get; set; }
|
||||
public bool RequireNonEmpty { get; set; }
|
||||
public bool RequireAllItems { get; set; }
|
||||
|
||||
private readonly ImmutableArray<Identifier> gearTags;
|
||||
@@ -61,7 +61,7 @@ namespace Barotrauma
|
||||
AllowStealing = AllowStealing,
|
||||
ignoredIdentifiersOrTags = ignoredTags,
|
||||
CheckPathForEachItem = CheckPathForEachItem,
|
||||
RequireLoaded = RequireLoaded,
|
||||
RequireNonEmpty = RequireNonEmpty,
|
||||
ItemCount = count,
|
||||
SpeakIfFails = RequireAllItems
|
||||
},
|
||||
|
||||
+7
-6
@@ -364,8 +364,7 @@ namespace Barotrauma
|
||||
CurrentOrders.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
var currentOrderInfo = character.GetCurrentOrder(currentOrder);
|
||||
if (currentOrderInfo is Order)
|
||||
if (character.GetCurrentOrder(currentOrder) is Order currentOrderInfo)
|
||||
{
|
||||
int currentPriority = currentOrderInfo.ManualPriority;
|
||||
if (currentOrder.ManualPriority != currentPriority)
|
||||
@@ -539,7 +538,8 @@ namespace Barotrauma
|
||||
KeepActiveWhenReady = true,
|
||||
CheckInventory = true,
|
||||
Equip = false,
|
||||
FindAllItems = true
|
||||
FindAllItems = true,
|
||||
RequireNonEmpty = false
|
||||
};
|
||||
break;
|
||||
case "findweapon":
|
||||
@@ -555,7 +555,8 @@ namespace Barotrauma
|
||||
KeepActiveWhenReady = false,
|
||||
CheckInventory = false,
|
||||
EvaluateCombatPriority = true,
|
||||
FindAllItems = false
|
||||
FindAllItems = false,
|
||||
RequireNonEmpty = true
|
||||
};
|
||||
}
|
||||
prepareObjective.KeepActiveWhenReady = false;
|
||||
@@ -600,9 +601,9 @@ namespace Barotrauma
|
||||
|
||||
Order dismissOrder = currentOrder.GetDismissal();
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
|
||||
if (GameMain.GameSession?.CrewManager is CrewManager cm && cm.IsSinglePlayer)
|
||||
{
|
||||
GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder);
|
||||
character.SetOrder(dismissOrder, isNewOrder: true, speak: false);
|
||||
}
|
||||
#else
|
||||
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, character, character));
|
||||
|
||||
+4
-3
@@ -27,6 +27,7 @@ namespace Barotrauma
|
||||
public bool FindAllItems { get; set; }
|
||||
public bool Equip { get; set; }
|
||||
public bool EvaluateCombatPriority { get; set; }
|
||||
public bool RequireNonEmpty { get; set; }
|
||||
|
||||
private AIObjective GetSubObjective()
|
||||
{
|
||||
@@ -74,7 +75,7 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
|
||||
}
|
||||
else if (items.Any(i => i.Components.Any(i => !i.IsLoaded(character))))
|
||||
else if (items.Any(i => i.Components.Any(i => !i.IsNotEmpty(character))))
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
@@ -106,7 +107,7 @@ namespace Barotrauma
|
||||
CheckInventory = CheckInventory,
|
||||
Equip = Equip,
|
||||
EvaluateCombatPriority = EvaluateCombatPriority,
|
||||
RequireLoaded = true,
|
||||
RequireNonEmpty = RequireNonEmpty,
|
||||
RequireAllItems = requireAll
|
||||
},
|
||||
onCompleted: () =>
|
||||
@@ -157,7 +158,7 @@ namespace Barotrauma
|
||||
{
|
||||
EvaluateCombatPriority = EvaluateCombatPriority,
|
||||
SpeakIfFails = true,
|
||||
RequireLoaded = true
|
||||
RequireNonEmpty = RequireNonEmpty
|
||||
};
|
||||
}
|
||||
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
|
||||
|
||||
+4
-16
@@ -320,10 +320,10 @@ namespace Barotrauma
|
||||
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
|
||||
{
|
||||
if (treatmentSuitability.Value <= cprSuitability) { continue; }
|
||||
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab)
|
||||
if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab))
|
||||
{
|
||||
if (!Item.ItemList.Any(it => ((MapEntity)it).Prefab.Identifier == treatmentSuitability.Key)) { continue; }
|
||||
suitableItemIdentifiers.Add(treatmentSuitability.Key);
|
||||
if (Item.ItemList.None(it => it.Prefab.Identifier == treatmentSuitability.Key)) { continue; }
|
||||
suitableItemIdentifiers.Add(itemPrefab.Identifier);
|
||||
//only list the first 4 items
|
||||
if (itemNameList.Count < 4)
|
||||
{
|
||||
@@ -413,7 +413,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void ApplyTreatment(Affliction affliction, Item item)
|
||||
{
|
||||
item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
|
||||
@@ -482,18 +482,6 @@ namespace Barotrauma
|
||||
|
||||
public static IEnumerable<Affliction> GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs);
|
||||
|
||||
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
|
||||
{
|
||||
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
|
||||
foreach (Affliction affliction in allAfflictions)
|
||||
{
|
||||
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
|
||||
if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; }
|
||||
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
|
||||
yield return affliction;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
|
||||
+22
-3
@@ -26,7 +26,7 @@ namespace Barotrauma
|
||||
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
|
||||
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
|
||||
// On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled.
|
||||
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
|
||||
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer && target.HealthPercentage < 100 ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,15 +67,34 @@ namespace Barotrauma
|
||||
float vitality = 100;
|
||||
vitality -= character.Bleeding * 2;
|
||||
vitality += Math.Min(character.Oxygen, 0);
|
||||
vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis");
|
||||
foreach (Affliction affliction in AIObjectiveRescue.GetTreatableAfflictions(character))
|
||||
foreach (Affliction affliction in GetTreatableAfflictions(character))
|
||||
{
|
||||
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
|
||||
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
|
||||
if (affliction.Prefab.AfflictionType == "paralysis")
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
else if (affliction.Prefab.AfflictionType == "poison")
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
}
|
||||
return Math.Clamp(vitality, 0, 100);
|
||||
}
|
||||
|
||||
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
|
||||
{
|
||||
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
|
||||
foreach (Affliction affliction in allAfflictions)
|
||||
{
|
||||
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
|
||||
if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; }
|
||||
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
|
||||
yield return affliction;
|
||||
}
|
||||
}
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Character target)
|
||||
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
|
||||
|
||||
|
||||
@@ -371,7 +371,8 @@ namespace Barotrauma
|
||||
private int CalculateCellCount(int minValue, int maxValue)
|
||||
{
|
||||
if (maxValue == 0) { return 0; }
|
||||
float t = MathUtils.InverseLerp(0, 100, Level.Loaded.Difficulty * Config.AgentSpawnCountDifficultyMultiplier);
|
||||
float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
|
||||
float t = MathUtils.InverseLerp(0, 100, difficulty * Config.AgentSpawnCountDifficultyMultiplier);
|
||||
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t));
|
||||
}
|
||||
|
||||
@@ -381,7 +382,8 @@ namespace Barotrauma
|
||||
float delay = Config.AgentSpawnDelay;
|
||||
float min = delay;
|
||||
float max = delay * 6;
|
||||
float t = Level.Loaded.Difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor);
|
||||
float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
|
||||
float t = difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor);
|
||||
return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t));
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Barotrauma
|
||||
public bool IsAiming => wasAiming;
|
||||
public bool IsAimingMelee => wasAimingMelee;
|
||||
|
||||
protected bool Aiming => aiming || aimingMelee || LockFlippingUntil > Timing.TotalTime && character.IsKeyDown(InputType.Aim);
|
||||
protected bool Aiming => aiming || aimingMelee || FlipLockTime > Timing.TotalTime && character.IsKeyDown(InputType.Aim);
|
||||
|
||||
public float ArmLength => upperArmLength + forearmLength;
|
||||
|
||||
@@ -278,7 +278,11 @@ namespace Barotrauma
|
||||
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
|
||||
public bool IsAboveFloor => GetHeightFromFloor() > -0.1f;
|
||||
|
||||
public float LockFlippingUntil;
|
||||
public float FlipLockTime { get; private set; }
|
||||
public void LockFlipping(float time = 0.2f)
|
||||
{
|
||||
FlipLockTime = (float)Timing.TotalTime + time;
|
||||
}
|
||||
|
||||
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
|
||||
{
|
||||
|
||||
@@ -994,7 +994,7 @@ namespace Barotrauma
|
||||
foreach (Limb l in Limbs)
|
||||
{
|
||||
if (l.IsSevered) { continue; }
|
||||
if (!l.DoesFlip) { continue; }
|
||||
if (!l.DoesFlip) { continue; }
|
||||
if (RagdollParams.IsSpritesheetOrientationHorizontal)
|
||||
{
|
||||
//horizontally aligned limbs need to be flipped 180 degrees
|
||||
@@ -1014,7 +1014,7 @@ namespace Barotrauma
|
||||
if (l.IsSevered) { continue; }
|
||||
|
||||
float rotation = l.body.Rotation;
|
||||
if (l.DoesFlip)
|
||||
if (l.DoesMirror)
|
||||
{
|
||||
if (RagdollParams.IsSpritesheetOrientationHorizontal)
|
||||
{
|
||||
|
||||
+56
-22
@@ -150,8 +150,10 @@ namespace Barotrauma
|
||||
|
||||
private readonly float movementLerp;
|
||||
|
||||
private float cprAnimTimer;
|
||||
private float cprPump;
|
||||
private float cprAnimTimer,cprPump;
|
||||
|
||||
private float fallingProneAnimTimer;
|
||||
const float FallingProneAnimDuration = 1.0f;
|
||||
|
||||
private bool swimming;
|
||||
//time until the character can switch from walking to swimming or vice versa
|
||||
@@ -268,7 +270,8 @@ namespace Barotrauma
|
||||
if (deathAnimTimer < deathAnimDuration)
|
||||
{
|
||||
deathAnimTimer += deltaTime;
|
||||
UpdateDying(deltaTime);
|
||||
//the force/torque used to move the limbs goes from 1 to 0 during the death anim duration
|
||||
UpdateFallingProne(1.0f - deathAnimTimer / deathAnimDuration);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -278,6 +281,11 @@ namespace Barotrauma
|
||||
|
||||
if (!character.CanMove)
|
||||
{
|
||||
if (fallingProneAnimTimer < FallingProneAnimDuration)
|
||||
{
|
||||
fallingProneAnimTimer += deltaTime;
|
||||
UpdateFallingProne(1.0f);
|
||||
}
|
||||
levitatingCollider = false;
|
||||
Collider.FarseerBody.FixedRotation = false;
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
@@ -291,18 +299,20 @@ namespace Barotrauma
|
||||
}
|
||||
return;
|
||||
}
|
||||
fallingProneAnimTimer = 0.0f;
|
||||
|
||||
//re-enable collider
|
||||
if (!Collider.Enabled)
|
||||
{
|
||||
var lowestLimb = FindLowestLimb();
|
||||
|
||||
|
||||
Collider.SetTransform(new Vector2(
|
||||
Collider.SimPosition.X,
|
||||
Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)),
|
||||
Collider.Rotation);
|
||||
|
||||
Collider.FarseerBody.ResetDynamics();
|
||||
Collider.FarseerBody.LinearVelocity = MainLimb.LinearVelocity;
|
||||
Collider.Enabled = true;
|
||||
}
|
||||
|
||||
@@ -431,7 +441,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck)
|
||||
if (Timing.TotalTime > FlipLockTime && TargetDir != dir && !IsStuck)
|
||||
{
|
||||
Flip();
|
||||
}
|
||||
@@ -444,7 +454,7 @@ namespace Barotrauma
|
||||
aiming = false;
|
||||
wasAimingMelee = aimingMelee;
|
||||
aimingMelee = false;
|
||||
IsHanging = false;
|
||||
IsHanging = IsHanging && character.IsRagdolled;
|
||||
}
|
||||
|
||||
void UpdateStanding()
|
||||
@@ -1292,10 +1302,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateDying(float deltaTime)
|
||||
void UpdateFallingProne(float strength)
|
||||
{
|
||||
//the force/torque used to move the limbs goes from 1 to 0 during the death anim duration
|
||||
float strength = 1.0f - deathAnimTimer / deathAnimDuration;
|
||||
if (strength <= 0.0f) { return; }
|
||||
|
||||
Limb head = GetLimb(LimbType.Head);
|
||||
Limb torso = GetLimb(LimbType.Torso);
|
||||
@@ -1319,6 +1328,19 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
if (torso == null) { return; }
|
||||
|
||||
//make the torso tip over
|
||||
//otherwise it tends to just drop straight down, pinning the characters legs in a weird pose
|
||||
if (!InWater)
|
||||
{
|
||||
//prefer tipping over in the same direction the torso is rotating
|
||||
//or moving
|
||||
//or lastly, in the direction it's facing if it's not moving/rotating
|
||||
float fallDirection = Math.Sign(torso.body.AngularVelocity - torso.body.LinearVelocity.X - Dir * 0.01f);
|
||||
float torque = MathF.Cos(torso.Rotation) * fallDirection * 5.0f * strength;
|
||||
torso.body.ApplyTorque(torque * torso.body.Mass);
|
||||
}
|
||||
|
||||
//attempt to make legs stay in a straight line with the torso to prevent the character from doing a split
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
@@ -1503,12 +1525,12 @@ namespace Barotrauma
|
||||
Limb rightHand = GetLimb(LimbType.RightHand);
|
||||
|
||||
Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm);
|
||||
if (targetLeftHand == null) targetLeftHand = target.AnimController.GetLimb(LimbType.Torso);
|
||||
if (targetLeftHand == null) targetLeftHand = target.AnimController.MainLimb;
|
||||
if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); }
|
||||
if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; }
|
||||
|
||||
Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm);
|
||||
if (targetRightHand == null) targetRightHand = target.AnimController.GetLimb(LimbType.Torso);
|
||||
if (targetRightHand == null) targetRightHand = target.AnimController.MainLimb;
|
||||
if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); }
|
||||
if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; }
|
||||
|
||||
if (!target.AllowInput)
|
||||
{
|
||||
@@ -1644,18 +1666,24 @@ namespace Barotrauma
|
||||
pullLimb.PullJointEnabled = true;
|
||||
if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb)
|
||||
{
|
||||
Vector2 pullLimbAnchor = targetLimb.SimPosition;
|
||||
pullLimb.PullJointMaxForce = 5000.0f;
|
||||
if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging))
|
||||
{
|
||||
targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f);
|
||||
}
|
||||
|
||||
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);
|
||||
Vector2 shoulderPos = rightShoulder.WorldAnchorA;
|
||||
float targetDist = Vector2.Distance(targetLimb.SimPosition, shoulderPos);
|
||||
Vector2 dragDir = (targetLimb.SimPosition - shoulderPos) / targetDist;
|
||||
if (!MathUtils.IsValid(dragDir)) { dragDir = -Vector2.UnitY; }
|
||||
if (!InWater)
|
||||
{
|
||||
//lerp the arm downwards when not swimming
|
||||
dragDir = Vector2.Lerp(dragDir, -Vector2.UnitY, 0.5f);
|
||||
}
|
||||
|
||||
Vector2 pullLimbAnchor = shoulderPos + dragDir * Math.Min(targetDist, (upperArmLength + forearmLength) * 2);
|
||||
targetAnchor = shoulderPos + dragDir * (upperArmLength + forearmLength);
|
||||
targetForce = 200.0f;
|
||||
if (target.Submarine != character.Submarine)
|
||||
{
|
||||
@@ -1723,7 +1751,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X)
|
||||
{
|
||||
target.AnimController.LockFlippingUntil = (float)Timing.TotalTime + 0.5f;
|
||||
target.AnimController.LockFlipping(0.5f);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1822,16 +1850,22 @@ namespace Barotrauma
|
||||
|
||||
public override void Flip()
|
||||
{
|
||||
if (Character == null || Character.Removed)
|
||||
{
|
||||
LogAccessedRemovedCharacterError();
|
||||
return;
|
||||
}
|
||||
|
||||
base.Flip();
|
||||
|
||||
WalkPos = -WalkPos;
|
||||
|
||||
Limb torso = GetLimb(LimbType.Torso);
|
||||
|
||||
Vector2 difference;
|
||||
if (torso == null) { return; }
|
||||
|
||||
Matrix torsoTransform = Matrix.CreateRotationZ(torso.Rotation);
|
||||
|
||||
Vector2 difference;
|
||||
foreach (Item heldItem in character.HeldItems)
|
||||
{
|
||||
if (heldItem?.body != null && !heldItem.Removed && heldItem.GetComponent<Holdable>() != null)
|
||||
|
||||
@@ -57,17 +57,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (limbs == null)
|
||||
{
|
||||
if (!accessRemovedCharacterErrorShown)
|
||||
{
|
||||
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
|
||||
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"Ragdoll.Limbs:AccessRemoved",
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
accessRemovedCharacterErrorShown = true;
|
||||
}
|
||||
LogAccessedRemovedCharacterError();
|
||||
return Array.Empty<Limb>();
|
||||
}
|
||||
return limbs;
|
||||
@@ -158,6 +148,20 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetCollider(int index, out PhysicsBody collider)
|
||||
{
|
||||
collider = null;
|
||||
try
|
||||
{
|
||||
collider = this.collider?[index];
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int ColliderIndex
|
||||
{
|
||||
get
|
||||
@@ -873,7 +877,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (Limb limb in Limbs)
|
||||
{
|
||||
if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; }
|
||||
if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; }
|
||||
limb.Dir = Dir;
|
||||
limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y);
|
||||
limb.MirrorPullJoint();
|
||||
@@ -1428,6 +1432,21 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void LogAccessedRemovedCharacterError()
|
||||
{
|
||||
if (!accessRemovedCharacterErrorShown)
|
||||
{
|
||||
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
|
||||
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"Ragdoll:AccessRemoved",
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
accessRemovedCharacterErrorShown = true;
|
||||
}
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime, Camera cam);
|
||||
|
||||
partial void Splash(Limb limb, Hull limbHull);
|
||||
|
||||
@@ -492,37 +492,52 @@ namespace Barotrauma
|
||||
DamageParticles(deltaTime, worldPosition);
|
||||
|
||||
var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult();
|
||||
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
|
||||
var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
|
||||
var additionalEffectType = ActionType.OnUse;
|
||||
if (targetCharacter != null && targetCharacter.IsDead)
|
||||
{
|
||||
effectType = ActionType.OnEating;
|
||||
additionalEffectType = ActionType.OnEating;
|
||||
}
|
||||
|
||||
foreach (StatusEffect effect in statusEffects)
|
||||
{
|
||||
effect.sourceBody = sourceBody;
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
// TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied
|
||||
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity, worldPosition);
|
||||
var t = sourceLimb ?? attacker as ISerializableEntity;
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, t, worldPosition);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, attacker, t, worldPosition);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Parent))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, attacker, attacker);
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, attacker);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, attacker, attacker);
|
||||
}
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetCharacter, attackResult.HitLimb);
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
|
||||
// TODO: do we need the conversion to list here? It generates garbage.
|
||||
var targets = targetCharacter.AnimController.Limbs.Cast<ISerializableEntity>().ToList();
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetCharacter, targets);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, targetCharacter, targets);
|
||||
}
|
||||
}
|
||||
if (target is Entity targetEntity)
|
||||
@@ -532,18 +547,30 @@ namespace Barotrauma
|
||||
{
|
||||
targets.Clear();
|
||||
effect.AddNearbyTargets(worldPosition, targets);
|
||||
effect.Apply(effectType, deltaTime, targetEntity, targets);
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetEntity, targets);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, targetEntity, targets);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition);
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition);
|
||||
}
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
|
||||
{
|
||||
targets.Clear();
|
||||
targets.AddRange(attacker.Inventory.AllItems);
|
||||
effect.Apply(effectType, deltaTime, attacker, targets);
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, targets);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, attacker, targets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,47 +606,52 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration);
|
||||
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
|
||||
var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
|
||||
|
||||
foreach (StatusEffect effect in statusEffects)
|
||||
{
|
||||
effect.sourceBody = sourceBody;
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Parent))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, attacker, attacker);
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, attacker);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, attacker, attacker);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character);
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb.character);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb.character);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb);
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
|
||||
// TODO: do we need the conversion to list here? It generates garbage.
|
||||
var targets = targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList();
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
targets.Clear();
|
||||
effect.AddNearbyTargets(worldPosition, targets);
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targets);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition);
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
|
||||
{
|
||||
targets.Clear();
|
||||
targets.AddRange(attacker.Inventory.AllItems);
|
||||
effect.Apply(effectType, deltaTime, attacker, targets);
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, targets);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, attacker, targets);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly static List<Character> CharacterList = new List<Character>();
|
||||
|
||||
public const float MaxHighlightDistance = 150.0f;
|
||||
public const float MaxDragDistance = 200.0f;
|
||||
|
||||
partial void UpdateLimbLightSource(Limb limb);
|
||||
|
||||
private bool enabled = true;
|
||||
@@ -354,9 +357,15 @@ namespace Barotrauma
|
||||
public readonly CharacterPrefab Prefab;
|
||||
|
||||
public readonly CharacterParams Params;
|
||||
|
||||
public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier();
|
||||
|
||||
public Identifier Group => Params.Group;
|
||||
|
||||
public bool IsHumanoid => Params.Humanoid;
|
||||
|
||||
public bool IsMachine => Params.IsMachine;
|
||||
|
||||
public bool IsHusk => Params.Husk;
|
||||
|
||||
public bool IsMale => info?.IsMale ?? false;
|
||||
@@ -480,7 +489,7 @@ namespace Barotrauma
|
||||
LocalizedString displayName = Params.DisplayName;
|
||||
if (displayName.IsNullOrWhiteSpace())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride))
|
||||
if (Params.SpeciesTranslationOverride.IsEmpty)
|
||||
{
|
||||
displayName = TextManager.Get($"Character.{SpeciesName}");
|
||||
}
|
||||
@@ -743,7 +752,7 @@ namespace Barotrauma
|
||||
get
|
||||
{
|
||||
if (IsUnconscious) { return true; }
|
||||
return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
|
||||
return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.Identifier == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -805,9 +814,15 @@ namespace Barotrauma
|
||||
public float HealthPercentage => CharacterHealth.HealthPercentage;
|
||||
public float MaxVitality => CharacterHealth.MaxVitality;
|
||||
public float MaxHealth => MaxVitality;
|
||||
|
||||
/// <summary>
|
||||
/// Was the character in full health at the beginning of the frame?
|
||||
/// </summary>
|
||||
public bool WasFullHealth => CharacterHealth.WasInFullHealth;
|
||||
public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
|
||||
public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached;
|
||||
public float EmpVulnerability => Params.Health.EmpVulnerability;
|
||||
public float PoisonVulnerability => Params.Health.PoisonVulnerability;
|
||||
|
||||
public float Bloodloss
|
||||
{
|
||||
@@ -1026,6 +1041,8 @@ namespace Barotrauma
|
||||
|
||||
public bool InWater => AnimController is AnimController { InWater: true };
|
||||
|
||||
public bool IsLowInOxygen => NeedsOxygen && OxygenAvailable < CharacterHealth.LowOxygenThreshold;
|
||||
|
||||
public bool GodMode = false;
|
||||
|
||||
public CampaignMode.InteractionType CampaignInteractionType;
|
||||
@@ -1741,7 +1758,7 @@ namespace Barotrauma
|
||||
float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed);
|
||||
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
|
||||
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
|
||||
SpeedMultiplier = greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier);
|
||||
SpeedMultiplier = Math.Max(0.0f, greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier));
|
||||
targetMovement *= SpeedMultiplier;
|
||||
// Reset, status effects will set the value before the next update
|
||||
ResetSpeedMultiplier();
|
||||
@@ -2201,7 +2218,9 @@ namespace Barotrauma
|
||||
|
||||
if (SelectedCharacter != null)
|
||||
{
|
||||
if (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > 90000.0f || !SelectedCharacter.CanBeSelected)
|
||||
if (!SelectedCharacter.CanBeSelected ||
|
||||
(Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > MaxDragDistance * MaxDragDistance &&
|
||||
SelectedCharacter.GetDistanceToClosestLimb(SimPosition) > ConvertUnits.ToSimUnits(MaxDragDistance)))
|
||||
{
|
||||
DeselectCharacter();
|
||||
}
|
||||
@@ -2494,8 +2513,12 @@ namespace Barotrauma
|
||||
|
||||
if (!skipDistanceCheck)
|
||||
{
|
||||
maxDist = ConvertUnits.ToSimUnits(maxDist);
|
||||
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; }
|
||||
maxDist = Math.Max(ConvertUnits.ToSimUnits(maxDist), c.AnimController.Collider.GetMaxExtent());
|
||||
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist &&
|
||||
Vector2.DistanceSquared(SimPosition, c.AnimController.MainLimb.SimPosition) > maxDist * maxDist)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return checkVisibility ? CanSeeCharacter(c) : true;
|
||||
@@ -2851,6 +2874,23 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
#if CLIENT
|
||||
if (Controlled == this)
|
||||
{
|
||||
HealingCooldown.PutOnCooldown();
|
||||
}
|
||||
#elif SERVER
|
||||
if (GameMain.Server?.ConnectedClients is { } clients)
|
||||
{
|
||||
foreach (Client c in clients)
|
||||
{
|
||||
if (c.Character != this) { continue; }
|
||||
|
||||
HealingCooldown.SetCooldown(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
SelectCharacter(FocusedCharacter);
|
||||
#if CLIENT
|
||||
if (Controlled == this)
|
||||
@@ -3138,56 +3178,57 @@ namespace Barotrauma
|
||||
|
||||
UpdateAIChatMessages(deltaTime);
|
||||
|
||||
//Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us!
|
||||
bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true;
|
||||
bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 8.0f * 8.0f;
|
||||
bool wasRagdolled = false;
|
||||
bool selfRagdolled = false;
|
||||
|
||||
if (IsForceRagdolled)
|
||||
if (GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true)
|
||||
{
|
||||
IsRagdolled = IsForceRagdolled;
|
||||
}
|
||||
else if (this != Controlled)
|
||||
{
|
||||
wasRagdolled = IsRagdolled;
|
||||
IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll);
|
||||
}
|
||||
//Keep us ragdolled if we were forced or we're too speedy to unragdoll
|
||||
else if (allowRagdoll && (!IsRagdolled || !tooFastToUnragdoll))
|
||||
{
|
||||
if (ragdollingLockTimer > 0.0f)
|
||||
bool wasRagdolled = IsRagdolled;
|
||||
if (IsForceRagdolled)
|
||||
{
|
||||
SetInput(InputType.Ragdoll, false, true);
|
||||
ragdollingLockTimer -= deltaTime;
|
||||
IsRagdolled = IsForceRagdolled;
|
||||
}
|
||||
else if (this != Controlled)
|
||||
{
|
||||
wasRagdolled = IsRagdolled;
|
||||
IsRagdolled = IsKeyDown(InputType.Ragdoll);
|
||||
}
|
||||
else
|
||||
{
|
||||
wasRagdolled = IsRagdolled;
|
||||
IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
|
||||
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.5f; }
|
||||
bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body);
|
||||
bool bodyMovingTooFast(PhysicsBody body)
|
||||
{
|
||||
return
|
||||
body.LinearVelocity.LengthSquared() > 8.0f * 8.0f ||
|
||||
//falling down counts as going too fast
|
||||
(!InWater && body.LinearVelocity.Y < -5.0f);
|
||||
}
|
||||
if (ragdollingLockTimer > 0.0f)
|
||||
{
|
||||
ragdollingLockTimer -= deltaTime;
|
||||
}
|
||||
else if (!tooFastToUnragdoll)
|
||||
{
|
||||
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
|
||||
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; }
|
||||
}
|
||||
if (IsRagdolled)
|
||||
{
|
||||
SetInput(InputType.Ragdoll, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasRagdolled && IsRagdolled)
|
||||
{
|
||||
if (selfRagdolled)
|
||||
if (!wasRagdolled && IsRagdolled)
|
||||
{
|
||||
CheckTalents(AbilityEffectType.OnSelfRagdoll);
|
||||
CheckTalents(AbilityEffectType.OnRagdoll);
|
||||
}
|
||||
// currently does not work when you are stunned, like it should
|
||||
CheckTalents(AbilityEffectType.OnRagdoll);
|
||||
}
|
||||
|
||||
lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f);
|
||||
|
||||
//ragdoll button
|
||||
if (IsRagdolled || !CanMove)
|
||||
{
|
||||
if (AnimController is HumanoidAnimController humanAnimController)
|
||||
{
|
||||
humanAnimController.Crouching = false;
|
||||
}
|
||||
if (IsRagdolled) { AnimController.IgnorePlatforms = true; }
|
||||
AnimController.ResetPullJoints();
|
||||
SelectedItem = SelectedSecondaryItem = null;
|
||||
return;
|
||||
@@ -3365,6 +3406,20 @@ namespace Barotrauma
|
||||
return distSqr;
|
||||
}
|
||||
|
||||
public float GetDistanceToClosestLimb(Vector2 simPos)
|
||||
{
|
||||
float closestDist = float.MaxValue;
|
||||
foreach (Limb limb in AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
float dist = Vector2.Distance(simPos, limb.SimPosition);
|
||||
dist -= limb.body.GetMaxExtent();
|
||||
closestDist = Math.Min(closestDist, dist);
|
||||
if (closestDist <= 0.0f) { return 0.0f; }
|
||||
}
|
||||
return closestDist;
|
||||
}
|
||||
|
||||
private float despawnTimer;
|
||||
private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true)
|
||||
{
|
||||
@@ -3756,9 +3811,10 @@ namespace Barotrauma
|
||||
message.SendDelay -= deltaTime;
|
||||
if (message.SendDelay > 0.0f) { continue; }
|
||||
|
||||
bool canUseRadio = ChatMessage.CanUseRadio(this, out WifiComponent radio);
|
||||
if (message.MessageType == null)
|
||||
{
|
||||
message.MessageType = ChatMessage.CanUseRadio(this) ? ChatMessageType.Radio : ChatMessageType.Default;
|
||||
message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default;
|
||||
}
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
|
||||
@@ -3768,6 +3824,11 @@ namespace Barotrauma
|
||||
{
|
||||
GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this);
|
||||
}
|
||||
if (canUseRadio)
|
||||
{
|
||||
Signal s = new Signal(modifiedMessage, sender: this, source: radio.Item);
|
||||
radio.TransmitSignal(s, sentFromChat: true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if SERVER
|
||||
@@ -3885,8 +3946,8 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Affliction affliction in attackResult.Afflictions)
|
||||
{
|
||||
if (affliction.Strength == 0.0f) continue;
|
||||
sb.Append($" {affliction.Prefab.Name}: {affliction.Strength}");
|
||||
if (Math.Abs(affliction.Strength) <= 0.1f) { continue;}
|
||||
sb.Append($" {affliction.Prefab.Name}: {affliction.Strength.ToString("0.0")}");
|
||||
}
|
||||
}
|
||||
GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack);
|
||||
@@ -4087,13 +4148,13 @@ namespace Barotrauma
|
||||
OnAttackedProjSpecific(attacker, attackResult, stun);
|
||||
if (!wasDead)
|
||||
{
|
||||
TryAdjustAttackerSkill(attacker, CharacterHealth.Vitality - prevVitality);
|
||||
TryAdjustAttackerSkill(attacker, attackResult);
|
||||
}
|
||||
};
|
||||
}
|
||||
if (attackResult.Damage > 0)
|
||||
{
|
||||
LastDamage = attackResult;
|
||||
if (attacker != null)
|
||||
if (attacker != null && attacker != this && !attacker.Removed)
|
||||
{
|
||||
AddAttacker(attacker, attackResult.Damage);
|
||||
AddEncounter(attacker);
|
||||
@@ -4108,26 +4169,84 @@ namespace Barotrauma
|
||||
|
||||
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun);
|
||||
|
||||
public void TryAdjustAttackerSkill(Character attacker, float healthChange)
|
||||
public void TryAdjustAttackerSkill(Character attacker, AttackResult attackResult)
|
||||
{
|
||||
if (attacker == null) { return; }
|
||||
|
||||
if (!attacker.IsOnPlayerTeam) { return; }
|
||||
bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID;
|
||||
if (isEnemy)
|
||||
if (!isEnemy) { return; }
|
||||
float weaponDamage = 0;
|
||||
float medicalDamage = 0;
|
||||
foreach (var affliction in attackResult.Afflictions)
|
||||
{
|
||||
if (healthChange < 0.0f)
|
||||
if (affliction.Prefab.IsBuff) { continue; }
|
||||
if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; }
|
||||
if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis")
|
||||
{
|
||||
float attackerSkillLevel = attacker.GetSkillLevel("weapons");
|
||||
attacker.Info?.IncreaseSkillLevel("weapons".ToIdentifier(),
|
||||
-healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f));
|
||||
if (!Params.Health.PoisonImmunity)
|
||||
{
|
||||
float relativeVitality = MaxVitality / 100f;
|
||||
// Undo the applied modifiers to get the base value. Poison damage is multiplied by max vitality when it's applied.
|
||||
float dmg = affliction.Strength;
|
||||
if (relativeVitality > 0)
|
||||
{
|
||||
dmg /= relativeVitality;
|
||||
}
|
||||
if (PoisonVulnerability > 0)
|
||||
{
|
||||
dmg /= PoisonVulnerability;
|
||||
}
|
||||
float strength = MaxVitality;
|
||||
if (Params.AI != null)
|
||||
{
|
||||
strength = Params.AI.CombatStrength;
|
||||
}
|
||||
// Adjust the skill gain by the strength of the target. Combat strength >= 1000 gives 2x bonus, combat strength < 333 less than 1x.
|
||||
float vitalityFactor = MathHelper.Lerp(0.5f, 2f, MathUtils.InverseLerp(0, 1000, strength));
|
||||
dmg *= vitalityFactor;
|
||||
medicalDamage += dmg * affliction.Prefab.MedicalSkillGain;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
medicalDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.MedicalSkillGain;
|
||||
}
|
||||
weaponDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.WeaponsSkillGain;
|
||||
}
|
||||
else if (healthChange > 0.0f)
|
||||
if (medicalDamage > 0)
|
||||
{
|
||||
float attackerSkillLevel = attacker.GetSkillLevel("medical");
|
||||
attacker.Info?.IncreaseSkillLevel("medical".ToIdentifier(),
|
||||
healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f));
|
||||
IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage);
|
||||
}
|
||||
if (weaponDamage > 0)
|
||||
{
|
||||
IncreaseSkillLevel("weapons".ToIdentifier(), weaponDamage);
|
||||
}
|
||||
|
||||
void IncreaseSkillLevel(Identifier skill, float damage)
|
||||
{
|
||||
float attackerSkillLevel = attacker.GetSkillLevel(skill);
|
||||
// The formula is too generous on low skill levels, hence the minimum divider.
|
||||
float minSkillDivider = 15f;
|
||||
attacker.Info?.IncreaseSkillLevel(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, minSkillDivider));
|
||||
}
|
||||
}
|
||||
|
||||
public void TryAdjustHealerSkill(Character healer, float healthChange = 0, Affliction affliction = null)
|
||||
{
|
||||
if (healer == null) { return; }
|
||||
bool isEnemy = AIController is EnemyAIController || TeamID != healer.TeamID;
|
||||
if (isEnemy) { return; }
|
||||
float medicalGain = healthChange;
|
||||
if (affliction?.Prefab is { IsBuff: true } && (!Params.IsMachine || affliction.Prefab.AffectMachines))
|
||||
{
|
||||
medicalGain += affliction.Strength * affliction.Prefab.MedicalSkillGain;
|
||||
}
|
||||
if (medicalGain <= 0) { return; }
|
||||
Identifier skill = new Identifier("medical");
|
||||
float attackerSkillLevel = healer.GetSkillLevel(skill);
|
||||
// The formula is too generous on low skill levels, hence the minimum divider.
|
||||
float minSkillDivider = 15f;
|
||||
healer.Info?.IncreaseSkillLevel(skill, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, minSkillDivider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -4484,7 +4603,10 @@ namespace Barotrauma
|
||||
|
||||
#if CLIENT
|
||||
//ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns
|
||||
Inventory?.ApplyReceivedState();
|
||||
if (GameMain.Client?.ClientPeer is { IsActive: true })
|
||||
{
|
||||
Inventory?.ApplyReceivedState();
|
||||
}
|
||||
#endif
|
||||
|
||||
base.Remove();
|
||||
@@ -5200,15 +5322,30 @@ namespace Barotrauma
|
||||
|
||||
public void RemoveAbilityResistance(TalentResistanceIdentifier identifier) => abilityResistances.Remove(identifier);
|
||||
|
||||
/// <summary>
|
||||
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
|
||||
/// </summary>
|
||||
public bool IsFriendly(Character other) => IsFriendly(this, other);
|
||||
|
||||
/// <summary>
|
||||
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
|
||||
/// </summary>
|
||||
public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
|
||||
public static bool IsFriendly(Character me, Character other) => IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other);
|
||||
|
||||
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
|
||||
{
|
||||
if (myTeam == otherTeam) { return true; }
|
||||
return myTeam switch
|
||||
{
|
||||
// NPCs are friendly to the same team and the friendly NPCs
|
||||
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
|
||||
// Friendly NPCs are friendly to both player teams
|
||||
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
|
||||
public bool IsOnFriendlyTeam(Character other) => IsOnFriendlyTeam(TeamID, other.TeamID);
|
||||
public bool IsOnFriendlyTeam(CharacterTeamType otherTeam) => IsOnFriendlyTeam(TeamID, otherTeam);
|
||||
|
||||
public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other);
|
||||
|
||||
public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
|
||||
|
||||
public void StopClimbing()
|
||||
{
|
||||
|
||||
@@ -778,9 +778,10 @@ namespace Barotrauma
|
||||
FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
|
||||
SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
|
||||
|
||||
Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.White);
|
||||
Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.White);
|
||||
Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White);
|
||||
//default to transparent color, it's invalid and will be replaced with a random one in CheckColors
|
||||
Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.Transparent);
|
||||
Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.Transparent);
|
||||
Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Transparent);
|
||||
CheckColors();
|
||||
|
||||
TryLoadNameAndTitle(npcIdentifier);
|
||||
|
||||
+37
-4
@@ -321,6 +321,7 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
|
||||
public readonly float MinInterval, MaxInterval;
|
||||
public readonly float MinStrength, MaxStrength;
|
||||
|
||||
public PeriodicEffect(ContentXElement element, string parentDebugName)
|
||||
{
|
||||
@@ -335,8 +336,10 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
MinInterval = Math.Max(element.GetAttributeFloat("mininterval", 1.0f), 1.0f);
|
||||
MaxInterval = Math.Max(element.GetAttributeFloat("maxinterval", 1.0f), MinInterval);
|
||||
MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 1.0f);
|
||||
MaxInterval = Math.Max(element.GetAttributeFloat(nameof(MaxInterval), 1.0f), MinInterval);
|
||||
MinStrength = Math.Max(element.GetAttributeFloat(nameof(MinStrength), 0f), 0f);
|
||||
MaxStrength = Math.Max(element.GetAttributeFloat(nameof(MaxStrength), MinStrength), MinStrength);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -415,8 +418,8 @@ namespace Barotrauma
|
||||
//how much karma changes when a player applies this affliction to someone (per strength of the affliction)
|
||||
public float KarmaChangeOnApplied;
|
||||
|
||||
public float BurnOverlayAlpha;
|
||||
public float DamageOverlayAlpha;
|
||||
public readonly float BurnOverlayAlpha;
|
||||
public readonly float DamageOverlayAlpha;
|
||||
|
||||
//steam achievement given when the affliction is removed from the controlled character
|
||||
public readonly Identifier AchievementOnRemoved;
|
||||
@@ -427,6 +430,20 @@ namespace Barotrauma
|
||||
public readonly Sprite AfflictionOverlay;
|
||||
public readonly bool AfflictionOverlayAlphaIsLinear;
|
||||
|
||||
public readonly bool DamageParticles;
|
||||
|
||||
/// <summary>
|
||||
/// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target.
|
||||
/// If the affliction causes damage or is of type poison or paralysis, the skill is increased only when the target is hostile.
|
||||
/// If the affliction is of type buff, the skill is increased only when the target is friendly.
|
||||
/// </summary>
|
||||
public readonly float MedicalSkillGain;
|
||||
/// <summary>
|
||||
/// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target.
|
||||
/// The skill is increased only when the target is hostile.
|
||||
/// </summary>
|
||||
public readonly float WeaponsSkillGain;
|
||||
|
||||
private readonly List<Effect> effects = new List<Effect>();
|
||||
private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>();
|
||||
|
||||
@@ -528,6 +545,10 @@ namespace Barotrauma
|
||||
|
||||
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
|
||||
|
||||
DamageParticles = element.GetAttributeBool(nameof(DamageParticles), true);
|
||||
WeaponsSkillGain = element.GetAttributeFloat(nameof(WeaponsSkillGain), 0.0f);
|
||||
MedicalSkillGain = element.GetAttributeFloat(nameof(MedicalSkillGain), 0.0f);
|
||||
|
||||
List<Description> descriptions = new List<Description>();
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
@@ -602,6 +623,18 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < effects.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < effects.Count; j++)
|
||||
{
|
||||
var a = effects[i];
|
||||
var b = effects[j];
|
||||
if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength)
|
||||
{
|
||||
DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearEffects()
|
||||
|
||||
@@ -230,6 +230,11 @@ namespace Barotrauma
|
||||
|
||||
public float StunTimer { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Was the character in full health at the beginning of the frame?
|
||||
/// </summary>
|
||||
public bool WasInFullHealth { get; private set; }
|
||||
|
||||
public Affliction PressureAffliction
|
||||
{
|
||||
get { return pressureAffliction; }
|
||||
@@ -703,7 +708,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; }
|
||||
if (Character.Params.Health.PoisonImmunity && (newAffliction.Prefab.AfflictionType == "poison" || newAffliction.Prefab.AfflictionType == "paralysis")) { return; }
|
||||
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; }
|
||||
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
|
||||
{
|
||||
@@ -772,6 +777,8 @@ namespace Barotrauma
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
WasInFullHealth = vitality >= MaxVitality;
|
||||
|
||||
UpdateOxygen(deltaTime);
|
||||
|
||||
StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
|
||||
@@ -878,7 +885,11 @@ namespace Barotrauma
|
||||
|
||||
private void UpdateOxygen(float deltaTime)
|
||||
{
|
||||
if (!Character.NeedsOxygen) { return; }
|
||||
if (!Character.NeedsOxygen)
|
||||
{
|
||||
oxygenLowAffliction.Strength = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
|
||||
float prevOxygen = OxygenAmount;
|
||||
@@ -980,6 +991,8 @@ namespace Barotrauma
|
||||
UpdateLimbAfflictionOverlays();
|
||||
UpdateSkinTint();
|
||||
Character.Kill(type, affliction);
|
||||
|
||||
WasInFullHealth = false;
|
||||
#if CLIENT
|
||||
DisplayVitalityDelay = 0.0f;
|
||||
DisplayedVitality = Vitality;
|
||||
@@ -1024,17 +1037,18 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private readonly List<Affliction> allAfflictions = new List<Affliction>();
|
||||
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions)
|
||||
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
|
||||
{
|
||||
allAfflictions.Clear();
|
||||
if (!mergeSameAfflictions)
|
||||
{
|
||||
allAfflictions.AddRange(afflictions.Keys);
|
||||
allAfflictions.AddRange(predicate == null ? afflictions.Keys : afflictions.Keys.Where(predicate));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (Affliction affliction in afflictions.Keys)
|
||||
{
|
||||
if (predicate != null && !predicate(affliction)) { continue; }
|
||||
var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab);
|
||||
if (existingAffliction == null)
|
||||
{
|
||||
|
||||
@@ -200,7 +200,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
partial class Limb : ISerializableEntity, ISpatialEntity
|
||||
{
|
||||
//how long it takes for severed limbs to fade out
|
||||
@@ -215,7 +215,7 @@ namespace Barotrauma
|
||||
|
||||
//the physics body of the limb
|
||||
public PhysicsBody body;
|
||||
|
||||
|
||||
public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale;
|
||||
|
||||
public Hull Hull;
|
||||
@@ -249,7 +249,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private bool isSevered;
|
||||
private float severedFadeOutTimer;
|
||||
|
||||
@@ -269,7 +269,7 @@ namespace Barotrauma
|
||||
mouthPos = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public readonly Attack attack;
|
||||
public List<DamageModifier> DamageModifiers { get; private set; } = new List<DamageModifier>();
|
||||
|
||||
@@ -282,39 +282,73 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (character.AnimController.CurrentAnimationParams is GroundedMovementParams)
|
||||
if (character?.AnimController.CurrentAnimationParams is GroundedMovementParams && IsLeg)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case LimbType.LeftFoot:
|
||||
case LimbType.LeftLeg:
|
||||
case LimbType.LeftThigh:
|
||||
case LimbType.RightFoot:
|
||||
case LimbType.RightLeg:
|
||||
case LimbType.RightThigh:
|
||||
// Legs always has to flip
|
||||
return true;
|
||||
}
|
||||
// Legs always has to flip when not swimming
|
||||
return true;
|
||||
}
|
||||
return Params.Flip;
|
||||
}
|
||||
}
|
||||
|
||||
public bool DoesMirror
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsLeg)
|
||||
{
|
||||
// Legs always has to mirror
|
||||
return true;
|
||||
}
|
||||
return DoesFlip;
|
||||
}
|
||||
}
|
||||
|
||||
public float SteerForce => Params.SteerForce;
|
||||
|
||||
public Vector2 DebugTargetPos;
|
||||
public Vector2 DebugRefPos;
|
||||
|
||||
public bool IsLowerBody =>
|
||||
type == LimbType.LeftLeg ||
|
||||
type == LimbType.RightLeg ||
|
||||
type == LimbType.LeftFoot ||
|
||||
type == LimbType.RightFoot ||
|
||||
type == LimbType.Tail ||
|
||||
type == LimbType.Legs ||
|
||||
type == LimbType.RightThigh ||
|
||||
type == LimbType.LeftThigh ||
|
||||
type == LimbType.Waist;
|
||||
public bool IsLowerBody
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case LimbType.LeftLeg:
|
||||
case LimbType.RightLeg:
|
||||
case LimbType.LeftFoot:
|
||||
case LimbType.RightFoot:
|
||||
case LimbType.Tail:
|
||||
case LimbType.Legs:
|
||||
case LimbType.LeftThigh:
|
||||
case LimbType.RightThigh:
|
||||
case LimbType.Waist:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLeg
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case LimbType.LeftFoot:
|
||||
case LimbType.LeftLeg:
|
||||
case LimbType.LeftThigh:
|
||||
case LimbType.RightFoot:
|
||||
case LimbType.RightLeg:
|
||||
case LimbType.RightThigh:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSevered
|
||||
{
|
||||
@@ -456,13 +490,9 @@ namespace Barotrauma
|
||||
|
||||
public int RefJointIndex => Params.RefJoint;
|
||||
|
||||
private List<WearableSprite> wearingItems;
|
||||
public List<WearableSprite> WearingItems
|
||||
{
|
||||
get { return wearingItems; }
|
||||
}
|
||||
public readonly List<WearableSprite> WearingItems = new List<WearableSprite>();
|
||||
|
||||
public List<WearableSprite> OtherWearables { get; private set; } = new List<WearableSprite>();
|
||||
public readonly List<WearableSprite> OtherWearables = new List<WearableSprite>();
|
||||
|
||||
public bool PullJointEnabled
|
||||
{
|
||||
@@ -606,7 +636,6 @@ namespace Barotrauma
|
||||
this.ragdoll = ragdoll;
|
||||
this.character = character;
|
||||
this.Params = limbParams;
|
||||
wearingItems = new List<WearableSprite>();
|
||||
dir = Direction.Right;
|
||||
body = new PhysicsBody(limbParams);
|
||||
type = limbParams.Type;
|
||||
@@ -738,7 +767,7 @@ namespace Barotrauma
|
||||
tempModifiers.Add(damageModifier);
|
||||
}
|
||||
}
|
||||
foreach (WearableSprite wearable in wearingItems)
|
||||
foreach (WearableSprite wearable in WearingItems)
|
||||
{
|
||||
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
|
||||
{
|
||||
@@ -757,10 +786,14 @@ namespace Barotrauma
|
||||
}
|
||||
if (!foundMatchingModifier && random > affliction.Probability) { continue; }
|
||||
float finalDamageModifier = damageMultiplier;
|
||||
if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0)
|
||||
if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == "emp")
|
||||
{
|
||||
finalDamageModifier *= character.EmpVulnerability;
|
||||
}
|
||||
if (!character.Params.Health.PoisonImmunity && (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis"))
|
||||
{
|
||||
finalDamageModifier *= character.PoisonVulnerability;
|
||||
}
|
||||
foreach (DamageModifier damageModifier in tempModifiers)
|
||||
{
|
||||
float damageModifierValue = damageModifier.DamageMultiplier;
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Barotrauma
|
||||
public Identifier SpeciesName { get; private set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable]
|
||||
public string SpeciesTranslationOverride { get; private set; }
|
||||
public Identifier SpeciesTranslationOverride { get; private set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
|
||||
public string DisplayName { get; private set; }
|
||||
@@ -501,6 +501,9 @@ namespace Barotrauma
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
public bool PoisonImmunity { get; set; }
|
||||
|
||||
[Serialize(1f, IsPropertySaveable.Yes, description: "1 = default, 0 = immune."), Editable(MinValueFloat = 0f, MaxValueFloat = 1000, DecimalCount = 1)]
|
||||
public float PoisonVulnerability { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable]
|
||||
public float EmpVulnerability { get; set; }
|
||||
|
||||
|
||||
+4
-3
@@ -75,13 +75,14 @@ namespace Barotrauma.Abilities
|
||||
if (wt == WeaponType.Any || !weapontype.HasFlag(wt)) { continue; }
|
||||
switch (wt)
|
||||
{
|
||||
// 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:
|
||||
//if the item has an active projectile component (has been fired), don't consider it a melee weapon
|
||||
if (item?.GetComponent<Projectile>() is { IsActive: true }) { continue; }
|
||||
if (item?.GetComponent<MeleeWeapon>() != null) { return true; }
|
||||
break;
|
||||
case WeaponType.Ranged:
|
||||
//if the item has a melee weapon component that's being used now, don't consider it a projectile
|
||||
if (item?.GetComponent<MeleeWeapon>() is { Hitting: true }) { continue; }
|
||||
if (item?.GetComponent<Projectile>() != null) { return true; }
|
||||
break;
|
||||
case WeaponType.HandheldRanged:
|
||||
|
||||
-1
@@ -28,7 +28,6 @@ namespace Barotrauma.Abilities
|
||||
conditionals.Add(new PropertyConditional(attribute));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-13
@@ -37,19 +37,7 @@ namespace Barotrauma.Abilities
|
||||
|
||||
if (itemPrefab != null)
|
||||
{
|
||||
if (category != MapEntityCategory.None)
|
||||
{
|
||||
if (!itemPrefab.Category.HasFlag(category)) { return false; }
|
||||
}
|
||||
|
||||
if (identifiers.Any())
|
||||
{
|
||||
if (!identifiers.Any(t => itemPrefab.Identifier == t))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p));
|
||||
return MatchesItem(itemPrefab);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -57,5 +45,22 @@ namespace Barotrauma.Abilities
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool MatchesItem(ItemPrefab itemPrefab)
|
||||
{
|
||||
if (category != MapEntityCategory.None)
|
||||
{
|
||||
if (!itemPrefab.Category.HasFlag(category)) { return false; }
|
||||
}
|
||||
|
||||
if (identifiers.Any())
|
||||
{
|
||||
if (!identifiers.Any(t => itemPrefab.Identifier == t))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -17,6 +17,14 @@ namespace Barotrauma.Abilities
|
||||
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
|
||||
}
|
||||
|
||||
public override void InitializeAbility(bool addingFirstTime)
|
||||
{
|
||||
if (addingFirstTime)
|
||||
{
|
||||
VerifyState(conditionsMatched: true, timeSinceLastUpdate: 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
|
||||
{
|
||||
if (conditionsMatched)
|
||||
|
||||
+19
-3
@@ -1,19 +1,35 @@
|
||||
#nullable enable
|
||||
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility
|
||||
{
|
||||
public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { }
|
||||
private readonly AbilityConditionItem? condition;
|
||||
|
||||
public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
var conditionElement = abilityElement.GetChildElement(nameof(AbilityConditionItem));
|
||||
if (conditionElement != null)
|
||||
{
|
||||
condition = new AbilityConditionItem(CharacterTalent, conditionElement);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
{
|
||||
if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; }
|
||||
|
||||
int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced);
|
||||
ingredients.Items.RemoveAt(randomIndex);
|
||||
List<Item> applicableIngredients = condition == null ?
|
||||
ingredients.Items.ToList() :
|
||||
ingredients.Items.Where(it => condition.MatchesItem(it.Prefab)).ToList();
|
||||
if (applicableIngredients.None()) { return; }
|
||||
|
||||
ingredients.Items.Remove(applicableIngredients.GetRandom(Rand.RandSync.Unsynced));
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-9
@@ -42,7 +42,7 @@ namespace Barotrauma
|
||||
public readonly Version GameVersion;
|
||||
public readonly string ModVersion;
|
||||
public Md5Hash Hash { get; private set; }
|
||||
public readonly Option<DateTime> InstallTime;
|
||||
public readonly Option<SerializableDateTime> InstallTime;
|
||||
|
||||
public ImmutableArray<ContentFile> Files { get; private set; }
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Barotrauma
|
||||
|
||||
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
|
||||
if (item is null) { return true; }
|
||||
return item.Value.LatestUpdateTime <= installTime;
|
||||
return item.Value.LatestUpdateTime <= installTime.ToUtcValue();
|
||||
}
|
||||
|
||||
public int Index => ContentPackageManager.EnabledPackages.IndexOf(this);
|
||||
@@ -106,10 +106,7 @@ namespace Barotrauma
|
||||
|
||||
GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version);
|
||||
ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion);
|
||||
UInt64 installTimeUnix = rootElement.GetAttributeUInt64("installtime", 0);
|
||||
InstallTime = installTimeUnix != 0
|
||||
? Option<DateTime>.Some(ToolBox.Epoch.ToDateTime(installTimeUnix))
|
||||
: Option<DateTime>.None();
|
||||
InstallTime = rootElement.GetAttributeDateTime("installtime");
|
||||
|
||||
var fileResults = rootElement.Elements()
|
||||
.Select(e => ContentFile.CreateFromXElement(this, e))
|
||||
@@ -288,9 +285,7 @@ namespace Barotrauma
|
||||
|
||||
if (errorCatcher.Errors.Any())
|
||||
{
|
||||
yield return ContentPackageManager.LoadProgress.Failure(
|
||||
ContentPackageManager.LoadProgress.Error
|
||||
.Reason.ConsoleErrorsThrown);
|
||||
yield return ContentPackageManager.LoadProgress.Failure(errorCatcher.Errors.Select(e => e.Text));
|
||||
yield break;
|
||||
}
|
||||
yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length);
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace Barotrauma
|
||||
public const string RegularPackagesElementName = "regularpackages";
|
||||
public const string RegularPackagesSubElementName = "package";
|
||||
|
||||
public static bool ModsEnabled => GameMain.VanillaContent == null || EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent);
|
||||
|
||||
public static class EnabledPackages
|
||||
{
|
||||
public static CorePackage? Core { get; private set; } = null;
|
||||
@@ -435,22 +437,19 @@ namespace Barotrauma
|
||||
public readonly record struct LoadProgress(Result<float, LoadProgress.Error> Result)
|
||||
{
|
||||
public readonly record struct Error(
|
||||
Error.Reason ErrorReason,
|
||||
Option<Exception> Exception)
|
||||
Either<ImmutableArray<string>, Exception> ErrorsOrException)
|
||||
{
|
||||
public enum Reason { Exception, ConsoleErrorsThrown }
|
||||
|
||||
public Error(Reason reason) : this(reason, Option.None) { }
|
||||
public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { }
|
||||
public Error(IEnumerable<string> errorMessages) : this(ErrorsOrException: errorMessages.ToImmutableArray()) { }
|
||||
public Error(Exception exception) : this(ErrorsOrException: exception) { }
|
||||
}
|
||||
|
||||
public static LoadProgress Failure(Exception exception)
|
||||
=> new LoadProgress(
|
||||
Result<float, Error>.Failure(new Error(exception)));
|
||||
|
||||
public static LoadProgress Failure(Error.Reason reason)
|
||||
public static LoadProgress Failure(IEnumerable<string> errorMessages)
|
||||
=> new LoadProgress(
|
||||
Result<float, Error>.Failure(new Error(reason)));
|
||||
Result<float, Error>.Failure(new Error(errorMessages)));
|
||||
|
||||
public static LoadProgress Progress(float value)
|
||||
=> new LoadProgress(
|
||||
|
||||
@@ -88,6 +88,7 @@ namespace Barotrauma
|
||||
public T GetAttributeEnum<T>(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def);
|
||||
public (T1, T2) GetAttributeTuple<T1, T2>(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def);
|
||||
public (T1, T2)[] GetAttributeTupleArray<T1, T2>(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def);
|
||||
public Range<int> GetAttributeRange(string key, in Range<int> def) => Element.GetAttributeRange(key, def);
|
||||
|
||||
public Identifier VariantOf() => Element.VariantOf();
|
||||
|
||||
|
||||
@@ -1312,10 +1312,10 @@ namespace Barotrauma
|
||||
}
|
||||
}, () =>
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
FactionPrefab.Prefabs.Select(f => f.Identifier.Value).ToArray(),
|
||||
GameMain.GameSession?.Campaign.Factions.Select(f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
|
||||
return new[]
|
||||
{
|
||||
FactionPrefab.Prefabs.Select(static f => f.Identifier.Value).ToArray(),
|
||||
GameMain.GameSession?.Campaign?.Factions.Select(static f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
|
||||
};
|
||||
}, true));
|
||||
|
||||
@@ -1868,6 +1868,7 @@ namespace Barotrauma
|
||||
commands.Add(new Command("followsub", "Toggle whether the camera should follow the nearest submarine (client-only).", null));
|
||||
commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true));
|
||||
commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true));
|
||||
commands.Add(new Command("devmode", "Toggle the dev mode on/off (client-only).", null, isCheat: true));
|
||||
|
||||
InitProjectSpecific();
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ namespace Barotrauma
|
||||
OnUseRangedWeapon,
|
||||
OnReduceAffliction,
|
||||
OnAddDamageAffliction,
|
||||
OnSelfRagdoll,
|
||||
OnRagdoll,
|
||||
OnRoundEnd,
|
||||
OnLootCharacter,
|
||||
|
||||
@@ -19,7 +19,9 @@ partial class UIHighlightAction : EventAction
|
||||
TurbineOutputSlider,
|
||||
DeconstructButton,
|
||||
RechargeSpeedSlider,
|
||||
CPRButton
|
||||
CPRButton,
|
||||
CloseButton,
|
||||
MessageBoxCloseButton
|
||||
}
|
||||
|
||||
[Serialize(ElementId.None, IsPropertySaveable.Yes)]
|
||||
|
||||
@@ -226,7 +226,7 @@ namespace Barotrauma
|
||||
|
||||
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
|
||||
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos, giveTags: true);
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos);
|
||||
|
||||
if (spawnPos is WayPoint wp)
|
||||
{
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace Barotrauma
|
||||
List<Submarine> connectedSubs = level.BeaconStation.GetConnectedSubs();
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (!connectedSubs.Contains(item.Submarine)) { continue; }
|
||||
if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; }
|
||||
if (item.GetComponent<PowerTransfer>() != null ||
|
||||
item.GetComponent<PowerContainer>() != null ||
|
||||
item.GetComponent<Reactor>() != null ||
|
||||
|
||||
@@ -404,7 +404,7 @@ namespace Barotrauma
|
||||
{
|
||||
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
|
||||
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
|
||||
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value));
|
||||
info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value));
|
||||
}
|
||||
|
||||
// apply money gains afterwards to prevent them from affecting XP gains
|
||||
@@ -545,17 +545,14 @@ namespace Barotrauma
|
||||
return humanPrefab;
|
||||
}
|
||||
|
||||
protected Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient, bool giveTags = true)
|
||||
protected static Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient)
|
||||
{
|
||||
var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient);
|
||||
characterInfo.TeamID = teamType;
|
||||
|
||||
if (positionToStayIn == null)
|
||||
{
|
||||
positionToStayIn =
|
||||
positionToStayIn ??=
|
||||
WayPoint.GetRandom(SpawnType.Human, characterInfo.Job?.Prefab, submarine) ??
|
||||
WayPoint.GetRandom(SpawnType.Human, null, submarine);
|
||||
}
|
||||
|
||||
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
|
||||
spawnedCharacter.HumanPrefab = humanPrefab;
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace Barotrauma
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(position.Position.ToVector2())))
|
||||
if (Level.Loaded.IsPositionInsideWall(position.Position.ToVector2()))
|
||||
{
|
||||
removals.Add(position);
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ namespace Barotrauma
|
||||
if (!SendUserStatistics) { return; }
|
||||
if (sentEventIdentifiers.Contains(identifier)) { return; }
|
||||
|
||||
if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent))
|
||||
if (ContentPackageManager.ModsEnabled)
|
||||
{
|
||||
message = "[MODDED] " + message;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Barotrauma
|
||||
for (int i = 0; i < Submarine.MainSubs.Length; i++)
|
||||
{
|
||||
var sub = Submarine.MainSubs[i];
|
||||
if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; }
|
||||
if (sub == null || sub.Info.InitialSuppliesSpawned || sub.Info.IsManuallyOutfitted || !sub.Info.IsPlayer) { continue; }
|
||||
//1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs)
|
||||
SpawnStartItems(sub, startItemSet);
|
||||
//2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc)
|
||||
|
||||
@@ -5,6 +5,7 @@ using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -13,13 +14,11 @@ namespace Barotrauma
|
||||
abstract partial class CampaignMode : GameMode
|
||||
{
|
||||
[NetworkSerialize]
|
||||
public struct SaveInfo : INetSerializableStruct
|
||||
{
|
||||
public string FilePath;
|
||||
public int SaveTime;
|
||||
public string SubmarineName;
|
||||
public string[] EnabledContentPackageNames;
|
||||
}
|
||||
public readonly record struct SaveInfo(
|
||||
string FilePath,
|
||||
Option<SerializableDateTime> SaveTime,
|
||||
string SubmarineName,
|
||||
ImmutableArray<string> EnabledContentPackageNames) : INetSerializableStruct;
|
||||
|
||||
public const int MaxMoney = int.MaxValue / 2; //about 1 billion
|
||||
public const int InitialMoney = 8500;
|
||||
@@ -1114,7 +1113,6 @@ namespace Barotrauma
|
||||
if (item.Components.None(c => c is Pickable)) { continue; }
|
||||
if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; }
|
||||
if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; }
|
||||
if (item.Container?.GetComponent<ItemContainer>() is { DrawInventory: false }) { continue; }
|
||||
itemsToTransfer.Add((item, item.Container));
|
||||
item.Submarine = null;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public const int DefaultMaxMissionCount = 2;
|
||||
public const int MaxMissionCountLimit = 10;
|
||||
public const int MaxMissionCountLimit = 3;
|
||||
public const int MinMissionCountLimit = 1;
|
||||
|
||||
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
|
||||
|
||||
@@ -46,15 +46,15 @@ namespace Barotrauma
|
||||
public static void Init()
|
||||
{
|
||||
#if CLIENT
|
||||
Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), true);
|
||||
DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), true);
|
||||
SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), true);
|
||||
TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), true);
|
||||
Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), isSinglePlayer: true);
|
||||
DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: true);
|
||||
SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), isSinglePlayer: true);
|
||||
TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), isSinglePlayer: true);
|
||||
#endif
|
||||
Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), false);
|
||||
Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), false);
|
||||
PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), false);
|
||||
MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), false, false);
|
||||
Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: false);
|
||||
Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), isSinglePlayer: false);
|
||||
PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), isSinglePlayer: false);
|
||||
MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), isSinglePlayer: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,9 +546,7 @@ namespace Barotrauma
|
||||
StatusEffect.StopAll();
|
||||
|
||||
#if CLIENT
|
||||
#if !DEBUG
|
||||
GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null;
|
||||
#endif
|
||||
GameMain.LightManager.LosEnabled = (GameMain.Client == null || GameMain.Client.CharacterInfo != null) && !GameMain.DevMode;
|
||||
if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; }
|
||||
if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; }
|
||||
#endif
|
||||
@@ -1074,7 +1072,10 @@ namespace Barotrauma
|
||||
XDocument doc = new XDocument(new XElement("Gamesession"));
|
||||
XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null.");
|
||||
|
||||
rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal));
|
||||
rootElement.Add(new XAttribute("savetime", SerializableDateTime.UtcNow.ToUnixTime()));
|
||||
#warning TODO: after this gets on main, replace savetime with the commented line
|
||||
//rootElement.Add(new XAttribute("savetime", SerializableDateTime.LocalNow));
|
||||
|
||||
rootElement.Add(new XAttribute("version", GameMain.Version));
|
||||
if (Submarine?.Info != null && !Submarine.Removed && Campaign != null)
|
||||
{
|
||||
|
||||
@@ -179,14 +179,41 @@ namespace Barotrauma
|
||||
|
||||
int price = prefab.Price.GetBuyPrice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation);
|
||||
int currentLevel = GetUpgradeLevel(prefab, category);
|
||||
int newLevel = currentLevel + 1;
|
||||
|
||||
int maxLevel = prefab.GetMaxLevelForCurrentSub();
|
||||
if (currentLevel + 1 > maxLevel)
|
||||
{
|
||||
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {maxLevel}). The transaction has been cancelled.");
|
||||
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({newLevel} > {maxLevel}). The transaction has been cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool TryTakeResources(Character character)
|
||||
{
|
||||
bool result = prefab.TryTakeResources(character, newLevel);
|
||||
if (!result)
|
||||
{
|
||||
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" but the player does not have the required resources.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
switch (GameMain.NetworkMember)
|
||||
{
|
||||
case null when Character.Controlled is { } controlled: // singleplayer
|
||||
if (!TryTakeResources(controlled)) { return; }
|
||||
break;
|
||||
case { IsClient: true }:
|
||||
if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return; }
|
||||
break;
|
||||
case { IsServer: true } when client?.Character is { } character:
|
||||
if (!TryTakeResources(character)) { return; }
|
||||
break;
|
||||
default:
|
||||
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" without a player.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (price < 0)
|
||||
{
|
||||
Location? location = Campaign.Map?.CurrentLocation;
|
||||
@@ -665,11 +692,13 @@ namespace Barotrauma
|
||||
/// Gets the progress that is shown on the store interface.
|
||||
/// Includes values stored in the metadata and <see cref="PendingUpgrades"/>, and takes submarine tier and class restrictions into account
|
||||
/// </summary>
|
||||
public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category)
|
||||
/// <param name="info">Submarine used to determine the upgrade limit. If not defined, will default to the current sub.</param>
|
||||
public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo? info = null)
|
||||
{
|
||||
if (!Metadata.HasKey(FormatIdentifier(prefab, category))) { return GetPendingLevel(); }
|
||||
|
||||
return Math.Min(GetRealUpgradeLevel(prefab, category) + GetPendingLevel(), prefab.GetMaxLevelForCurrentSub());
|
||||
int maxLevel = info is null ? prefab.GetMaxLevelForCurrentSub() : prefab.GetMaxLevel(info);
|
||||
return Math.Min(GetRealUpgradeLevel(prefab, category) + GetPendingLevel(), maxLevel);
|
||||
|
||||
int GetPendingLevel()
|
||||
{
|
||||
@@ -686,6 +715,14 @@ namespace Barotrauma
|
||||
return !Metadata.HasKey(FormatIdentifier(prefab, category)) ? 0 : Metadata.GetInt(FormatIdentifier(prefab, category), 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the level of the upgrade that is stored in the metadata. Takes into account the limits of the provided submarine.
|
||||
/// </summary>
|
||||
public int GetRealUpgradeLevelForSub(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo info)
|
||||
{
|
||||
return Math.Min(GetRealUpgradeLevel(prefab, category), prefab.GetMaxLevel(info));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the target upgrade level in the campaign metadata.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -308,6 +309,8 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
if (item.GetComponent<Pickable>() == null || item.AllowedSlots.None()) { return false; }
|
||||
|
||||
bool inSuitableSlot = false;
|
||||
bool inWrongSlot = false;
|
||||
int currentSlot = -1;
|
||||
|
||||
@@ -188,16 +188,26 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private DockingPort FindAdjacentPort()
|
||||
{
|
||||
float closestDist = float.MaxValue;
|
||||
DockingPort closestPort = null;
|
||||
foreach (DockingPort port in list)
|
||||
{
|
||||
if (port == this || port.item.Submarine == item.Submarine || port.IsHorizontal != IsHorizontal) { continue; }
|
||||
if (Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X) > DistanceTolerance.X) { continue; }
|
||||
if (Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y) > DistanceTolerance.Y) { continue; }
|
||||
float xDist = Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X);
|
||||
if (xDist > DistanceTolerance.X) { continue; }
|
||||
float yDist = Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y);
|
||||
if (yDist > DistanceTolerance.Y) { continue; }
|
||||
|
||||
return port;
|
||||
float dist = xDist + yDist;
|
||||
//disfavor non-interactable ports
|
||||
if (port.item.NonInteractable) { dist *= 2; }
|
||||
if (dist < closestDist)
|
||||
{
|
||||
closestPort = port;
|
||||
closestDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return closestPort;
|
||||
}
|
||||
|
||||
private void AttemptDock()
|
||||
@@ -279,7 +289,16 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(joint is WeldJoint))
|
||||
if (joint == null)
|
||||
{
|
||||
string errorMsg = "Error while locking a docking port (joint between submarines doesn't exist)." +
|
||||
" Submarine: " + (item.Submarine?.Info.Name ?? "null") +
|
||||
", target submarine: " + (DockingTarget.item.Submarine?.Info.Name ?? "null");
|
||||
GameAnalyticsManager.AddErrorEventOnce("DockingPort.Lock:JointNotCreated", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (joint is not WeldJoint)
|
||||
{
|
||||
DockingDir = GetDir(DockingTarget);
|
||||
DockingTarget.DockingDir = -DockingDir;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
#if CLIENT
|
||||
using Barotrauma.Lights;
|
||||
#endif
|
||||
@@ -206,6 +203,8 @@ namespace Barotrauma.Items.Components
|
||||
IsHorizontal = element.GetAttributeBool("horizontal", false);
|
||||
canBePicked = element.GetAttributeBool("canbepicked", false);
|
||||
autoOrientGap = element.GetAttributeBool("autoorientgap", false);
|
||||
|
||||
allowedSlots.Clear();
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
@@ -359,7 +358,11 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
lastBrokenTime = Timing.TotalTime;
|
||||
//the door has to be restored to 50% health before collision detection on the body is re-enabled
|
||||
if (item.ConditionPercentage > 50.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
||||
|
||||
//multiply by MaxRepairConditionMultiplier so the item gets repaired at 50% of the _default max condition_
|
||||
//otherwise increasing the max condition is arguably harmful, as the door needs to be repaired further to re-enable the collider
|
||||
if (item.ConditionPercentage * Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f &&
|
||||
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
||||
{
|
||||
IsBroken = false;
|
||||
}
|
||||
@@ -459,7 +462,10 @@ namespace Barotrauma.Items.Components
|
||||
ce = ce.Next;
|
||||
}
|
||||
}
|
||||
linkedGap.Open = 1.0f;
|
||||
if (linkedGap != null)
|
||||
{
|
||||
linkedGap.Open = 1.0f;
|
||||
}
|
||||
IsOpen = false;
|
||||
#if CLIENT
|
||||
if (convexHull != null) { convexHull.Enabled = false; }
|
||||
|
||||
@@ -58,9 +58,6 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
//the angle in which the Character holds the item
|
||||
protected float holdAngle;
|
||||
|
||||
public PhysicsBody Body
|
||||
{
|
||||
get { return item.body ?? body; }
|
||||
@@ -143,6 +140,7 @@ namespace Barotrauma.Items.Components
|
||||
set { aimPos = ConvertUnits.ToSimUnits(value); }
|
||||
}
|
||||
|
||||
protected float holdAngle;
|
||||
#if DEBUG
|
||||
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")]
|
||||
#else
|
||||
@@ -154,6 +152,18 @@ namespace Barotrauma.Items.Components
|
||||
set { holdAngle = MathHelper.ToRadians(value); }
|
||||
}
|
||||
|
||||
protected float aimAngle;
|
||||
#if DEBUG
|
||||
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item while aiming (in degrees, relative to the rotation of the character's hand).")]
|
||||
#else
|
||||
[Serialize(0.0f, IsPropertySaveable.No)]
|
||||
#endif
|
||||
public float AimAngle
|
||||
{
|
||||
get { return MathHelper.ToDegrees(aimAngle); }
|
||||
set { aimAngle = MathHelper.ToRadians(value); }
|
||||
}
|
||||
|
||||
private Vector2 swingAmount;
|
||||
#if DEBUG
|
||||
[Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")]
|
||||
@@ -552,6 +562,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool wasAttached = IsAttached;
|
||||
if (base.OnPicked(picker))
|
||||
{
|
||||
DeattachFromWall();
|
||||
@@ -560,7 +571,7 @@ namespace Barotrauma.Items.Components
|
||||
if (GameMain.Server != null && attachable)
|
||||
{
|
||||
item.CreateServerEvent(this);
|
||||
if (picker != null)
|
||||
if (picker != null && wasAttached)
|
||||
{
|
||||
GameServer.Log(GameServer.CharacterLogName(picker) + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction);
|
||||
}
|
||||
@@ -688,16 +699,22 @@ namespace Barotrauma.Items.Components
|
||||
if (maxAttachableCount == 0)
|
||||
{
|
||||
#if CLIENT
|
||||
GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red);
|
||||
if (character == Character.Controlled)
|
||||
{
|
||||
GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red);
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
else if (currentlyAttachedCount >= maxAttachableCount)
|
||||
{
|
||||
#if CLIENT
|
||||
GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red);
|
||||
if (character == Character.Controlled)
|
||||
{
|
||||
GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red);
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -875,9 +892,13 @@ namespace Barotrauma.Items.Components
|
||||
scaledHandlePos[0] = handlePos[0] * item.Scale;
|
||||
scaledHandlePos[1] = handlePos[1] * item.Scale;
|
||||
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim;
|
||||
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle);
|
||||
if (!aim)
|
||||
if (aim)
|
||||
{
|
||||
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle);
|
||||
}
|
||||
else
|
||||
{
|
||||
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle);
|
||||
var rope = GetRope();
|
||||
if (rope != null && rope.SnapWhenNotAimed && rope.Item.ParentInventory == null)
|
||||
{
|
||||
|
||||
@@ -49,6 +49,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Disable to make the weapon ignore all hit effects when it collides with walls, doors, or other items.")]
|
||||
public bool HitOnlyCharacters
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, IsPropertySaveable.No)]
|
||||
public bool Swing { get; set; }
|
||||
|
||||
@@ -112,7 +119,7 @@ namespace Barotrauma.Items.Components
|
||||
reloadTimer = reload;
|
||||
reloadTimer /= 1f + character.GetStatValue(StatTypes.MeleeAttackSpeed);
|
||||
reloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier);
|
||||
character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer * 0.9f;
|
||||
character.AnimController.LockFlipping();
|
||||
|
||||
item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile;
|
||||
item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking;
|
||||
@@ -216,10 +223,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
UpdateSwingPos(deltaTime, out Vector2 swingPos);
|
||||
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4));
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true);
|
||||
if (ac.InWater)
|
||||
{
|
||||
ac.LockFlippingUntil = (float)Timing.TotalTime + Reload;
|
||||
ac.LockFlipping();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -343,33 +350,36 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
hitTargets.Add(targetCharacter);
|
||||
}
|
||||
else if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure)
|
||||
else if (!HitOnlyCharacters)
|
||||
{
|
||||
if (AllowHitMultiple)
|
||||
if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure)
|
||||
{
|
||||
if (hitTargets.Contains(targetStructure)) { return true; }
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetStructure)) { return true; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Structure)) { return true; }
|
||||
}
|
||||
hitTargets.Add(targetStructure);
|
||||
}
|
||||
else
|
||||
else if (f2.Body.UserData is Item targetItem)
|
||||
{
|
||||
if (hitTargets.Any(t => t is Structure)) { return true; }
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetItem)) { return true; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Item)) { return true; }
|
||||
}
|
||||
hitTargets.Add(targetItem);
|
||||
}
|
||||
hitTargets.Add(targetStructure);
|
||||
}
|
||||
else if (f2.Body.UserData is Item targetItem)
|
||||
{
|
||||
if (AllowHitMultiple)
|
||||
else if (f2.Body.UserData is Holdable holdable && holdable.CanPush)
|
||||
{
|
||||
if (hitTargets.Contains(targetItem)) { return true; }
|
||||
hitTargets.Add(holdable.Item);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Item)) { return true; }
|
||||
}
|
||||
hitTargets.Add(targetItem);
|
||||
}
|
||||
else if (f2.Body.UserData is Holdable holdable && holdable.CanPush)
|
||||
{
|
||||
hitTargets.Add(holdable.Item);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -381,6 +391,7 @@ namespace Barotrauma.Items.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
private System.Text.StringBuilder serverLogger;
|
||||
private void HandleImpact(Fixture targetFixture)
|
||||
{
|
||||
var target = targetFixture.Body;
|
||||
@@ -398,11 +409,13 @@ namespace Barotrauma.Items.Components
|
||||
Character user = User;
|
||||
Limb targetLimb = target.UserData as Limb;
|
||||
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
|
||||
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
|
||||
Item targetItem = target.UserData as Item;
|
||||
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
|
||||
if (Attack != null)
|
||||
{
|
||||
Attack.SetUser(user);
|
||||
Attack.DamageMultiplier = damageMultiplier;
|
||||
|
||||
if (targetLimb != null)
|
||||
{
|
||||
if (targetLimb.character.Removed) { return; }
|
||||
@@ -415,12 +428,12 @@ namespace Barotrauma.Items.Components
|
||||
targetCharacter.LastDamageSource = item;
|
||||
Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if ((target.UserData as Structure ?? targetFixture.UserData as Structure) is Structure targetStructure)
|
||||
else if (targetStructure != null)
|
||||
{
|
||||
if (targetStructure.Removed) { return; }
|
||||
Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
|
||||
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
|
||||
{
|
||||
if (targetItem.Removed) { return; }
|
||||
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
|
||||
@@ -457,27 +470,43 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
conditionalActionType = ActionType.OnFailure;
|
||||
}
|
||||
if (GameMain.NetworkMember is { IsServer: true } server && targetCharacter != null)
|
||||
if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null)
|
||||
{
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb));
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb));
|
||||
#if SERVER
|
||||
if (GameMain.Server != null) //TODO: Log structure hits
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
|
||||
serverLogger ??= new System.Text.StringBuilder();
|
||||
serverLogger.Clear();
|
||||
serverLogger.Append($"{picker?.LogName} used {item.Name}");
|
||||
if (item.ContainedItems != null && item.ContainedItems.Any())
|
||||
{
|
||||
string logStr = picker?.LogName + " used " + item.Name;
|
||||
if (item.ContainedItems != null && item.ContainedItems.Any())
|
||||
{
|
||||
logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")";
|
||||
}
|
||||
logStr += " on " + targetCharacter.LogName + ".";
|
||||
Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack);
|
||||
serverLogger.Append($"({string.Join(", ", item.ContainedItems.Select(i => i?.Name))})");
|
||||
}
|
||||
#endif
|
||||
string targetName;
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
targetName = targetCharacter.LogName;
|
||||
}
|
||||
else if (targetItem != null)
|
||||
{
|
||||
targetName = targetItem.Name;
|
||||
}
|
||||
else if (targetStructure != null)
|
||||
{
|
||||
targetName = targetStructure.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetName = targetEntity.ToString();
|
||||
}
|
||||
serverLogger.Append($" on {targetName}.");
|
||||
#if SERVER
|
||||
Networking.GameServer.Log(serverLogger.ToString(), Networking.ServerLog.MessageType.Attack);
|
||||
#endif
|
||||
}
|
||||
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
|
||||
if (targetEntity != null)
|
||||
{
|
||||
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier);
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier);
|
||||
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
|
||||
}
|
||||
|
||||
if (DeleteOnUse)
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace Barotrauma.Items.Components
|
||||
get { return allowedSlots; }
|
||||
}
|
||||
|
||||
public bool PickingDone => pickTimer >= PickingTime;
|
||||
|
||||
public Character Picker
|
||||
{
|
||||
get
|
||||
|
||||
@@ -213,6 +213,8 @@ namespace Barotrauma.Items.Components
|
||||
baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure);
|
||||
}
|
||||
ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
|
||||
ReloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.FiringRateMultiplier);
|
||||
|
||||
currentChargeTime = 0f;
|
||||
|
||||
if (character != null)
|
||||
|
||||
@@ -4,7 +4,6 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.MapCreatures.Behavior;
|
||||
|
||||
@@ -185,24 +184,23 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
float degreeOfSuccess = character == null ? 0.5f : DegreeOfSuccess(character);
|
||||
|
||||
bool failed = false;
|
||||
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (UsableIn == UseEnvironment.None)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (item.InWater)
|
||||
{
|
||||
if (UsableIn == UseEnvironment.Air)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -210,9 +208,15 @@ namespace Barotrauma.Items.Components
|
||||
if (UsableIn == UseEnvironment.Water)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
if (failed)
|
||||
{
|
||||
// Always apply ActionType.OnUse. If doesn't fail, the effect is called later.
|
||||
ApplyStatusEffects(ActionType.OnUse, deltaTime, character);
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector2 rayStart;
|
||||
Vector2 rayStartWorld;
|
||||
@@ -312,9 +316,12 @@ namespace Barotrauma.Items.Components
|
||||
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair;
|
||||
|
||||
//if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them
|
||||
if (statusEffectLists != null && statusEffectLists.ContainsKey(ActionType.OnUse))
|
||||
if (statusEffectLists != null)
|
||||
{
|
||||
if (statusEffectLists[ActionType.OnUse].Any(s => s.SeverLimbsProbability > 0.0f))
|
||||
static bool CanSeverJoints(ActionType type, Dictionary<ActionType, List<StatusEffect>> effectList) =>
|
||||
effectList.TryGetValue(type, out List<StatusEffect> effects) && effects.Any(e => e.SeverLimbsProbability > 0);
|
||||
|
||||
if (CanSeverJoints(ActionType.OnUse, statusEffectLists) || CanSeverJoints(ActionType.OnSuccess, statusEffectLists))
|
||||
{
|
||||
float rangeSqr = ConvertUnits.ToSimUnits(Range);
|
||||
rangeSqr *= rangeSqr;
|
||||
@@ -537,6 +544,7 @@ namespace Barotrauma.Items.Components
|
||||
if (nonFixableEntities.Contains(targetStructure.Prefab.Identifier) || nonFixableEntities.Any(t => targetStructure.Tags.Contains(t))) { return false; }
|
||||
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, structure: targetStructure);
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, structure: targetStructure);
|
||||
FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex);
|
||||
|
||||
float structureFixAmount = StructureFixAmount;
|
||||
@@ -605,6 +613,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetCharacter, limb: closestLimb);
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetCharacter, limb: closestLimb);
|
||||
FixCharacterProjSpecific(user, deltaTime, targetCharacter);
|
||||
return true;
|
||||
}
|
||||
@@ -621,6 +630,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
targetLimb.character.LastDamageSource = item;
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetLimb.character, limb: targetLimb);
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetLimb.character, limb: targetLimb);
|
||||
FixCharacterProjSpecific(user, deltaTime, targetLimb.character);
|
||||
return true;
|
||||
}
|
||||
@@ -663,6 +673,7 @@ namespace Barotrauma.Items.Components
|
||||
targetItem.IsHighlighted = true;
|
||||
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem);
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, targetItem);
|
||||
|
||||
if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f))
|
||||
{
|
||||
@@ -875,7 +886,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
currentTargets.Add(character);
|
||||
currentTargets.Add(user);
|
||||
effect.Apply(actionType, deltaTime, item, currentTargets);
|
||||
}
|
||||
else if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
|
||||
@@ -205,12 +205,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (GameMain.NetworkMember is { IsServer: true })
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, CurrentThrower));
|
||||
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, targetCharacter: CurrentThrower));
|
||||
}
|
||||
if (!(GameMain.NetworkMember is { IsClient: true }))
|
||||
{
|
||||
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
|
||||
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower);
|
||||
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: CurrentThrower, user: CurrentThrower);
|
||||
}
|
||||
throwState = ThrowState.None;
|
||||
}
|
||||
|
||||
@@ -125,8 +125,8 @@ namespace Barotrauma.Items.Components
|
||||
get { return drawable; }
|
||||
set
|
||||
{
|
||||
if (value == drawable) return;
|
||||
if (!(this is IDrawableComponent))
|
||||
if (value == drawable) { return; }
|
||||
if (this is not IDrawableComponent)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't make \"" + this + "\" drawable (the component doesn't implement the IDrawableComponent interface)");
|
||||
return;
|
||||
@@ -236,10 +236,7 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).
|
||||
/// </summary>
|
||||
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).")]
|
||||
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). Note that there's also a generic BotPriority for all item prefabs.")]
|
||||
public float CombatPriority { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -697,7 +694,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public virtual void FlipY(bool relativeToSub) { }
|
||||
|
||||
public bool IsLoaded(Character user, bool checkContainedItems = true) =>
|
||||
public bool IsNotEmpty(Character user, bool checkContainedItems = true) =>
|
||||
HasRequiredContainedItems(user, addMessage: false) &&
|
||||
(!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
|
||||
|
||||
@@ -856,7 +853,13 @@ namespace Barotrauma.Items.Components
|
||||
if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; }
|
||||
if (user != null) { effect.SetUser(user); }
|
||||
effect.AfflictionMultiplier = afflictionMultiplier;
|
||||
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
|
||||
var c = character;
|
||||
if (user != null && effect.HasTargetType(StatusEffect.TargetType.Character) && !effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
// A bit hacky, but fixes MeleeWeapons targeting the use target instead of the attacker. Also applies to Projectiles and Throwables, or other callers that passes the user.
|
||||
c = user;
|
||||
}
|
||||
item.ApplyStatusEffect(effect, type, deltaTime, c, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
|
||||
effect.AfflictionMultiplier = 1.0f;
|
||||
reducesCondition |= effect.ReducesItemCondition();
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")]
|
||||
public int ItemsPerRow { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Should the contents in the item's inventory be visible? Disabled on items like magazines that spawn the contents as needed.")]
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")]
|
||||
public bool DrawInventory
|
||||
{
|
||||
get;
|
||||
@@ -142,6 +142,9 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No)]
|
||||
public bool AllowAccess { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
public bool AccessOnlyWhenBroken { get; set; }
|
||||
|
||||
@@ -534,12 +537,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
|
||||
{
|
||||
return DrawInventory && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
|
||||
return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
|
||||
}
|
||||
|
||||
public override bool Select(Character character)
|
||||
{
|
||||
if (!DrawInventory) { return false; }
|
||||
if (!AllowAccess) { return false; }
|
||||
if (item.Container != null) { return false; }
|
||||
if (AccessOnlyWhenBroken)
|
||||
{
|
||||
@@ -575,7 +578,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
if (!DrawInventory) { return false; }
|
||||
if (!AllowAccess) { return false; }
|
||||
if (AccessOnlyWhenBroken)
|
||||
{
|
||||
if (item.Condition > 0)
|
||||
|
||||
@@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components
|
||||
//disable flipping for 0.5 seconds, because flipping the character when it's in a weird pose (e.g. lying in bed) can mess up the ragdoll
|
||||
if (character.AnimController is HumanoidAnimController humanoidAnim)
|
||||
{
|
||||
humanoidAnim.LockFlippingUntil = (float)Timing.TotalTime + 0.5f;
|
||||
humanoidAnim.LockFlipping(0.5f);
|
||||
}
|
||||
|
||||
if (character.SelectedItem == item) { character.SelectedItem = null; }
|
||||
|
||||
@@ -112,27 +112,34 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")]
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to characters.")]
|
||||
public bool StickToCharacters
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the structure it hits.")]
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to walls.")]
|
||||
public bool StickToStructures
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the item it hits.")]
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to items.")]
|
||||
public bool StickToItems
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to doors. Caution: may cause issues.")]
|
||||
public bool StickToDoors
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick even to deflective targets.")]
|
||||
public bool StickToDeflective
|
||||
{
|
||||
@@ -457,36 +464,36 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 rayEndWorld = rayStartWorld + dir * worldDist;
|
||||
|
||||
List<HitscanResult> hits = new List<HitscanResult>();
|
||||
|
||||
hits.AddRange(DoRayCast(rayStart, rayEnd, submarine: item.Submarine));
|
||||
|
||||
if (item.Submarine != null)
|
||||
{
|
||||
//shooting indoors, do a hitscan outside as well
|
||||
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition, submarine: null));
|
||||
//also in the coordinate space of docked subs
|
||||
foreach (Submarine dockedSub in item.Submarine.DockedTo)
|
||||
{
|
||||
if (dockedSub == item.Submarine) { continue; }
|
||||
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition - dockedSub.SimPosition, rayEnd + item.Submarine.SimPosition - dockedSub.SimPosition, dockedSub));
|
||||
}
|
||||
//do a hitscan in other subs' coordinate spaces
|
||||
RayCastInOtherSubs(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
RayCastInOtherSubs(rayStart, rayEnd);
|
||||
}
|
||||
|
||||
void RayCastInOtherSubs(Vector2 rayStart, Vector2 rayEnd)
|
||||
{
|
||||
//shooting outdoors, see if we can hit anything inside a sub
|
||||
foreach (Submarine submarine in Submarine.Loaded)
|
||||
{
|
||||
if (submarine == item.Submarine) { continue; }
|
||||
var inSubHits = DoRayCast(rayStart - submarine.SimPosition, rayEnd - submarine.SimPosition, submarine);
|
||||
//transform back to world coordinates
|
||||
for (int i = 0; i < inSubHits.Count; i++)
|
||||
{
|
||||
inSubHits[i] = new HitscanResult(
|
||||
inSubHits[i].Fixture,
|
||||
inSubHits[i].Point + submarine.SimPosition,
|
||||
inSubHits[i].Normal,
|
||||
inSubHits[i].Fixture,
|
||||
inSubHits[i].Point + submarine.SimPosition,
|
||||
inSubHits[i].Normal,
|
||||
inSubHits[i].Fraction);
|
||||
}
|
||||
|
||||
hits.AddRange(inSubHits);
|
||||
}
|
||||
}
|
||||
@@ -767,7 +774,7 @@ namespace Barotrauma.Items.Components
|
||||
limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition);
|
||||
return false;
|
||||
}
|
||||
if (!FriendlyFire && User != null && limb.character.IsFriendly(User) && HumanAIController.IsOnFriendlyTeam(limb.character, User))
|
||||
if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -888,7 +895,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (target.Body.UserData is Limb limb)
|
||||
{
|
||||
if (!FriendlyFire && User != null && limb.character.IsFriendly(User) && HumanAIController.IsOnFriendlyTeam(limb.character, User))
|
||||
if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -959,8 +966,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (target.Body.UserData is Limb targetLimb)
|
||||
{
|
||||
ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: User);
|
||||
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: User);
|
||||
ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: User);
|
||||
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, useTarget: character, user: User);
|
||||
var attack = targetLimb.attack;
|
||||
if (attack != null)
|
||||
{
|
||||
@@ -971,22 +978,30 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
||||
{
|
||||
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
|
||||
effect.Apply(effect.type, 1.0f, User, User);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Character) || effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
targets.Clear();
|
||||
effect.AddNearbyTargets(targetLimb.WorldPosition, targets);
|
||||
effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets);
|
||||
effect.Apply(effect.type, 1.0f, targetLimb.character, targets);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (GameMain.NetworkMember is { IsServer: true } server)
|
||||
{
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, null, item.WorldPosition));
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition));
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition));
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition));
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -995,8 +1010,8 @@ namespace Barotrauma.Items.Components
|
||||
ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User);
|
||||
if (GameMain.NetworkMember is { IsServer: true } server)
|
||||
{
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition));
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1004,13 +1019,12 @@ namespace Barotrauma.Items.Components
|
||||
target.Body.ApplyLinearImpulse(velocity * item.body.Mass);
|
||||
target.Body.LinearVelocity = target.Body.LinearVelocity.ClampLength(NetConfig.MaxPhysicsBodyVelocity * 0.5f);
|
||||
|
||||
if (hits.Count() >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell)
|
||||
if (hits.Count >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell)
|
||||
{
|
||||
DisableProjectileCollisions();
|
||||
}
|
||||
|
||||
if (attackResult.AppliedDamageModifiers != null &&
|
||||
(attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective))
|
||||
if (attackResult.AppliedDamageModifiers != null && attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective)
|
||||
{
|
||||
item.body.LinearVelocity *= deflectedSpeedMultiplier;
|
||||
}
|
||||
@@ -1020,7 +1034,7 @@ namespace Barotrauma.Items.Components
|
||||
((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) &&
|
||||
(DoesStick ||
|
||||
(StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
|
||||
(StickToItems && target.Body.UserData is Item))))
|
||||
(target.Body.UserData is Item i && (i.GetComponent<Door>() != null ? StickToDoors : StickToItems)))))
|
||||
{
|
||||
Vector2 dir = new Vector2(
|
||||
(float)Math.Cos(item.body.Rotation),
|
||||
|
||||
@@ -83,7 +83,15 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (submarine?.Info == null || level == null || submarine.Info.Type == SubmarineType.Player) { return 0; }
|
||||
|
||||
float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, 1.0f);
|
||||
float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, level.LevelData.Biome.ActualMaxDifficulty / 100.0f);
|
||||
|
||||
if (level.Type == LevelData.LevelType.Outpost &&
|
||||
level.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
//no high-quality spawns in friendly outposts
|
||||
difficultyFactor = 0.0f;
|
||||
}
|
||||
|
||||
return ToolBox.SelectWeightedRandom(Enumerable.Range(0, MaxQuality + 1), q => GetCommonness(q, difficultyFactor), randSync);
|
||||
|
||||
static float GetCommonness(int quality, float difficultyFactor)
|
||||
|
||||
@@ -159,9 +159,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
var user = item.GetComponent<Projectile>()?.User;
|
||||
if (source == null || target == null || target.Removed ||
|
||||
(source is Entity sourceEntity && sourceEntity.Removed) ||
|
||||
(source is Limb limb && limb.Removed))
|
||||
(source is Limb limb && limb.Removed) ||
|
||||
(user != null && user.Removed))
|
||||
{
|
||||
ResetSource();
|
||||
target = null;
|
||||
@@ -293,7 +295,6 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
targetMass = float.MaxValue;
|
||||
}
|
||||
var user = item.GetComponent<Projectile>()?.User;
|
||||
if (targetMass > TargetMinMass)
|
||||
{
|
||||
if (Math.Abs(SourcePullForce) > 0.001f)
|
||||
@@ -301,33 +302,16 @@ namespace Barotrauma.Items.Components
|
||||
var sourceBody = GetBodyToPull(source);
|
||||
if (sourceBody != null)
|
||||
{
|
||||
var targetBody = GetBodyToPull(target);
|
||||
if (targetBody != null && !(targetBody.UserData is Character))
|
||||
if (user != null && user.InWater)
|
||||
{
|
||||
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
|
||||
}
|
||||
float forceMultiplier = 1;
|
||||
if (user != null)
|
||||
{
|
||||
user.AnimController.Hang();
|
||||
if (user.InWater)
|
||||
if (user.IsRagdolled)
|
||||
{
|
||||
if (user.IsRagdolled)
|
||||
{
|
||||
forceMultiplier = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
forceMultiplier = user.IsRagdolled ? 0.1f : 0.4f;
|
||||
// Prevents too easy smashing to the walls
|
||||
forceDir.X /= 4;
|
||||
// Prevents rubberbanding up and down
|
||||
if (forceDir.Y < 0)
|
||||
{
|
||||
forceDir.Y = 0;
|
||||
}
|
||||
// Reel in towards the target.
|
||||
user.AnimController.Hang();
|
||||
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) : SourcePullForce;
|
||||
sourceBody.ApplyForce(forceDir * force);
|
||||
}
|
||||
// Take the target velocity into account.
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
var myCollider = user.AnimController.Collider;
|
||||
@@ -340,9 +324,15 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var targetBody = GetBodyToPull(target);
|
||||
if (targetBody != null)
|
||||
{
|
||||
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
|
||||
}
|
||||
}
|
||||
}
|
||||
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) * forceMultiplier : SourcePullForce * forceMultiplier;
|
||||
sourceBody.ApplyForce(forceDir * force);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-13
@@ -94,7 +94,7 @@ namespace Barotrauma.Items.Components
|
||||
if (isOn == value && IsActive == value) { return; }
|
||||
|
||||
IsActive = isOn = value;
|
||||
SetLightSourceState(value, value ? lightBrightness : 0.0f);
|
||||
SetLightSourceState(value);
|
||||
OnStateChanged();
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@ namespace Barotrauma.Items.Components
|
||||
#if CLIENT
|
||||
if (Light != null)
|
||||
{
|
||||
Light.Color = IsOn ? lightColor.Multiply(currentBrightness) : Color.Transparent;
|
||||
Light.Color = IsOn ? lightColor.Multiply(lightBrightness) : Color.Transparent;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -187,6 +187,15 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Should the light sprite be drawn on the item using alpha blending, in addition to being rendered in the light map? Can be used to make the light sprite stand out more.")]
|
||||
public bool AlphaBlend
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public float TemporaryFlickerTimer;
|
||||
|
||||
public override void Move(Vector2 amount, bool ignoreContacts = false)
|
||||
{
|
||||
#if CLIENT
|
||||
@@ -205,7 +214,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (base.IsActive == value) { return; }
|
||||
base.IsActive = isOn = value;
|
||||
SetLightSourceState(value, value ? lightBrightness : 0.0f);
|
||||
SetLightSourceState(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,9 +245,10 @@ namespace Barotrauma.Items.Components
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
SetLightSourceState(IsActive, lightBrightness);
|
||||
SetLightSourceState(IsActive);
|
||||
turret = item.GetComponent<Turret>();
|
||||
#if CLIENT
|
||||
Drawable = AlphaBlend && Light.LightSprite != null;
|
||||
if (Screen.Selected.IsEditor)
|
||||
{
|
||||
OnMapLoaded();
|
||||
@@ -263,8 +273,7 @@ namespace Barotrauma.Items.Components
|
||||
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
|
||||
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
|
||||
{
|
||||
lightBrightness = 1.0f;
|
||||
SetLightSourceState(true, lightBrightness);
|
||||
SetLightSourceState(true);
|
||||
SetLightSourceTransformProjSpecific();
|
||||
base.IsActive = false;
|
||||
isOn = true;
|
||||
@@ -285,13 +294,15 @@ namespace Barotrauma.Items.Components
|
||||
UpdateAITarget(item.AiTarget);
|
||||
}
|
||||
UpdateOnActiveEffects(deltaTime);
|
||||
//something in UpdateOnActiveEffects may deactivate the light -> return so we don't turn it back on
|
||||
if (!IsActive) { return; }
|
||||
|
||||
#if CLIENT
|
||||
Light.ParentSub = item.Submarine;
|
||||
#endif
|
||||
if (item.Container != null && !(item.GetRootInventoryOwner() is Character))
|
||||
if (item.Container != null && item.GetRootInventoryOwner() is not Character)
|
||||
{
|
||||
SetLightSourceState(false, 0.0f);
|
||||
SetLightSourceState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -300,12 +311,14 @@ namespace Barotrauma.Items.Components
|
||||
PhysicsBody body = ParentBody ?? item.body;
|
||||
if (body != null && !body.Enabled)
|
||||
{
|
||||
SetLightSourceState(false, 0.0f);
|
||||
SetLightSourceState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
TemporaryFlickerTimer -= deltaTime;
|
||||
|
||||
//currPowerConsumption = powerConsumption;
|
||||
if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage))
|
||||
if (Rand.Range(0.0f, 1.0f) < 0.05f && (Voltage < Rand.Range(0.0f, MinVoltage) || TemporaryFlickerTimer > 0.0f))
|
||||
{
|
||||
#if CLIENT
|
||||
if (Voltage > 0.1f)
|
||||
@@ -325,7 +338,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
{
|
||||
SetLightSourceState(false, 0.0f);
|
||||
SetLightSourceState(false);
|
||||
}
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
@@ -357,7 +370,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
LightColor = XMLExtensions.ParseColor(signal.value, false);
|
||||
#if CLIENT
|
||||
SetLightSourceState(Light.Enabled, currentBrightness);
|
||||
SetLightSourceState(Light.Enabled);
|
||||
#endif
|
||||
prevColorSignal = signal.value;
|
||||
}
|
||||
@@ -375,7 +388,7 @@ namespace Barotrauma.Items.Components
|
||||
target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness);
|
||||
}
|
||||
|
||||
partial void SetLightSourceState(bool enabled, float brightness);
|
||||
partial void SetLightSourceState(bool enabled, float? brightness = null);
|
||||
|
||||
public void SetLightSourceTransform()
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private string prevSignal;
|
||||
|
||||
private readonly int[] channelMemory = new int[ChannelMemorySize];
|
||||
private int[] channelMemory = new int[ChannelMemorySize];
|
||||
|
||||
private Connection signalInConnection;
|
||||
private Connection signalOutConnection;
|
||||
@@ -94,7 +94,17 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
list.Add(this);
|
||||
IsActive = true;
|
||||
channelMemory = element.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]);
|
||||
}
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
channelMemory = componentElement.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]);
|
||||
if (channelMemory.Length != ChannelMemorySize)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error when loading item {item.Prefab.Identifier}: the size of the channel memory doesn't match the default value of {ChannelMemorySize}. Resizing...");
|
||||
Array.Resize(ref channelMemory, ChannelMemorySize);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable
|
||||
{
|
||||
partial class WireSection
|
||||
public partial class WireSection
|
||||
{
|
||||
private Vector2 start;
|
||||
private Vector2 end;
|
||||
@@ -775,20 +775,25 @@ namespace Barotrauma.Items.Components
|
||||
UpdateSections();
|
||||
}
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
public static IEnumerable<Vector2> ExtractNodes(XElement element)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
|
||||
string nodeString = componentElement.GetAttributeString("nodes", "");
|
||||
if (nodeString == "") return;
|
||||
string nodeString = element.GetAttributeString("nodes", "");
|
||||
if (nodeString.IsNullOrWhiteSpace()) { yield break; }
|
||||
|
||||
string[] nodeCoords = nodeString.Split(';');
|
||||
for (int i = 0; i < nodeCoords.Length / 2; i++)
|
||||
{
|
||||
float.TryParse(nodeCoords[i * 2], NumberStyles.Float, CultureInfo.InvariantCulture, out float x);
|
||||
float.TryParse(nodeCoords[i * 2 + 1], NumberStyles.Float, CultureInfo.InvariantCulture, out float y);
|
||||
nodes.Add(new Vector2(x, y));
|
||||
float.TryParse(nodeCoords[i * 2].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float x);
|
||||
float.TryParse(nodeCoords[i * 2 + 1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float y);
|
||||
yield return new Vector2(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
|
||||
nodes.AddRange(ExtractNodes(componentElement));
|
||||
|
||||
Drawable = nodes.Any();
|
||||
}
|
||||
|
||||
@@ -445,7 +445,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
// single charged shot guns will decharge after firing
|
||||
// for cosmetic reasons, this is done by lerping in half the reload time
|
||||
currentChargeTime = Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f));
|
||||
currentChargeTime = reloadTime > 0.0f ?
|
||||
Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f)) :
|
||||
0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -44,7 +44,16 @@ namespace Barotrauma
|
||||
}
|
||||
public LimbType Limb { get; private set; }
|
||||
public bool HideLimb { get; private set; }
|
||||
public bool HideOtherWearables { get; private set; }
|
||||
|
||||
public enum ObscuringMode
|
||||
{
|
||||
None,
|
||||
Hide,
|
||||
AlphaClip
|
||||
}
|
||||
public ObscuringMode ObscureOtherWearables { get; private set; }
|
||||
public bool HideOtherWearables => ObscureOtherWearables == ObscuringMode.Hide;
|
||||
public bool AlphaClipOtherWearables => ObscureOtherWearables == ObscuringMode.AlphaClip;
|
||||
public bool CanBeHiddenByOtherWearables { get; private set; }
|
||||
public List<WearableType> HideWearablesOfType { get; private set; }
|
||||
public bool InheritLimbDepth { get; private set; }
|
||||
@@ -130,7 +139,7 @@ namespace Barotrauma
|
||||
case WearableType.Husk:
|
||||
case WearableType.Herpes:
|
||||
Limb = LimbType.Head;
|
||||
HideOtherWearables = false;
|
||||
ObscureOtherWearables = ObscuringMode.None;
|
||||
InheritLimbDepth = true;
|
||||
InheritScale = true;
|
||||
InheritOrigin = true;
|
||||
@@ -202,7 +211,16 @@ namespace Barotrauma
|
||||
Sprite = new Sprite(SourceElement, file: SpritePath);
|
||||
Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true);
|
||||
HideLimb = SourceElement.GetAttributeBool("hidelimb", false);
|
||||
HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false);
|
||||
|
||||
foreach (var mode in Enum.GetValues<ObscuringMode>())
|
||||
{
|
||||
if (mode == ObscuringMode.None) { continue; }
|
||||
if (SourceElement.GetAttributeBool($"{mode}OtherWearables", false))
|
||||
{
|
||||
ObscureOtherWearables = mode;
|
||||
}
|
||||
}
|
||||
|
||||
CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true);
|
||||
InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true);
|
||||
var scale = SourceElement.GetAttribute("inheritscale");
|
||||
|
||||
@@ -268,6 +268,8 @@ namespace Barotrauma
|
||||
get { return capacity; }
|
||||
}
|
||||
|
||||
public int EmptySlotCount => slots.Count(i => !i.Empty());
|
||||
|
||||
public bool AllowSwappingContainedItems = true;
|
||||
|
||||
public Inventory(Entity owner, int capacity, int slotsPerRow = 5)
|
||||
@@ -583,6 +585,8 @@ namespace Barotrauma
|
||||
item.body.Enabled = false;
|
||||
item.body.BodyType = FarseerPhysics.BodyType.Dynamic;
|
||||
item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false);
|
||||
//update to refresh the interpolated draw rotation and position (update doesn't run on disabled bodies)
|
||||
item.body.Update();
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
@@ -887,10 +891,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (recursive)
|
||||
{
|
||||
if (item.OwnInventory != null)
|
||||
{
|
||||
item.OwnInventory.FindAllItems(predicate, recursive: true, list);
|
||||
}
|
||||
item.OwnInventory?.FindAllItems(predicate, recursive: true, list);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
|
||||
@@ -111,6 +111,7 @@ namespace Barotrauma
|
||||
private float sendConditionUpdateTimer;
|
||||
private bool conditionUpdatePending;
|
||||
|
||||
private float prevCondition;
|
||||
private float condition;
|
||||
|
||||
private bool inWater;
|
||||
@@ -896,7 +897,7 @@ namespace Barotrauma
|
||||
defaultRect = newRect;
|
||||
rect = newRect;
|
||||
|
||||
condition = MaxCondition = Prefab.Health;
|
||||
condition = MaxCondition = prevCondition = Prefab.Health;
|
||||
ConditionPercentage = 100.0f;
|
||||
|
||||
lastSentCondition = condition;
|
||||
@@ -998,13 +999,6 @@ namespace Barotrauma
|
||||
if (ic == null) break;
|
||||
|
||||
AddComponent(ic);
|
||||
|
||||
if (ic is IDrawableComponent && ic.Drawable)
|
||||
{
|
||||
drawableComponents.Add(ic as IDrawableComponent);
|
||||
hasComponentsToDraw = true;
|
||||
}
|
||||
if (ic is Repairable) repairables.Add((Repairable)ic);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1019,6 +1013,14 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (ic is Repairable repairable) { repairables.Add(repairable); }
|
||||
|
||||
if (ic is IDrawableComponent && ic.Drawable)
|
||||
{
|
||||
drawableComponents.Add(ic as IDrawableComponent);
|
||||
hasComponentsToDraw = true;
|
||||
}
|
||||
|
||||
if (ic.statusEffectLists == null) { continue; }
|
||||
if (ic.InheritStatusEffects)
|
||||
{
|
||||
@@ -1581,7 +1583,7 @@ namespace Barotrauma
|
||||
tags.Add(newTag);
|
||||
}
|
||||
|
||||
public IEnumerable<Identifier> GetTags()
|
||||
public IReadOnlyCollection<Identifier> GetTags()
|
||||
{
|
||||
return tags;
|
||||
}
|
||||
@@ -1743,15 +1745,15 @@ namespace Barotrauma
|
||||
if (Indestructible) { return; }
|
||||
if (InvulnerableToDamage && value <= condition) { return; }
|
||||
|
||||
float prev = condition;
|
||||
bool wasInFullCondition = IsFullCondition;
|
||||
|
||||
condition = MathHelper.Clamp(value, 0.0f, MaxCondition);
|
||||
if (MathUtils.NearlyEqual(prev, condition, epsilon: 0.000001f)) { return; }
|
||||
if (MathUtils.NearlyEqual(prevCondition, condition, epsilon: 0.000001f)) { return; }
|
||||
|
||||
RecalculateConditionValues();
|
||||
|
||||
if (condition == 0.0f && prev > 0.0f)
|
||||
bool wasPreviousConditionChanged = false;
|
||||
if (condition == 0.0f && prevCondition > 0.0f)
|
||||
{
|
||||
//Flag connections to be updated as device is broken
|
||||
flagChangedConnections(connections);
|
||||
@@ -1763,9 +1765,11 @@ namespace Barotrauma
|
||||
}
|
||||
if (Screen.Selected == GameMain.SubEditorScreen) { return; }
|
||||
#endif
|
||||
// Have to set the previous condition here or OnBroken status effects that reduce the condition will keep triggering the status effects, resulting in a stack overflow.
|
||||
SetPreviousCondition();
|
||||
ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
|
||||
}
|
||||
else if (condition > 0.0f && prev <= 0.0f)
|
||||
else if (condition > 0.0f && prevCondition <= 0.0f)
|
||||
{
|
||||
//Flag connections to be updated as device is now working again
|
||||
flagChangedConnections(connections);
|
||||
@@ -1793,8 +1797,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
LastConditionChange = condition - prev;
|
||||
ConditionLastUpdated = Timing.TotalTime;
|
||||
if (!wasPreviousConditionChanged)
|
||||
{
|
||||
SetPreviousCondition();
|
||||
}
|
||||
|
||||
void SetPreviousCondition()
|
||||
{
|
||||
LastConditionChange = condition - prevCondition;
|
||||
ConditionLastUpdated = Timing.TotalTime;
|
||||
prevCondition = condition;
|
||||
wasPreviousConditionChanged = true;
|
||||
}
|
||||
|
||||
static void flagChangedConnections(Dictionary<string, Connection> connections)
|
||||
{
|
||||
@@ -2159,8 +2173,9 @@ namespace Barotrauma
|
||||
var projectile = GetComponent<Projectile>();
|
||||
if (projectile != null)
|
||||
{
|
||||
//ignore character colliders (a projectile only hits limbs)
|
||||
if (f2.CollisionCategories == Physics.CollisionCharacter && f2.Body.UserData is Character) { return false; }
|
||||
// Ignore characters so that the impact sound only plays when the item hits a a wall or a door.
|
||||
// Projectile collisions are handled in Projectile.OnProjectileCollision(), so it should be safe to do this.
|
||||
if (f2.CollisionCategories == Physics.CollisionCharacter) { return false; }
|
||||
if (projectile.IgnoredBodies != null && projectile.IgnoredBodies.Contains(f2.Body)) { return false; }
|
||||
if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; }
|
||||
}
|
||||
@@ -2694,7 +2709,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (condition == 0.0f) { return; }
|
||||
if (condition <= 0.0f) { return; }
|
||||
|
||||
bool remove = false;
|
||||
|
||||
@@ -2711,7 +2726,7 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
ic.PlaySound(ActionType.OnUse, character);
|
||||
#endif
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb);
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: character, user: character);
|
||||
|
||||
if (ic.DeleteOnUse) { remove = true; }
|
||||
}
|
||||
@@ -2725,7 +2740,7 @@ namespace Barotrauma
|
||||
|
||||
public void SecondaryUse(float deltaTime, Character character = null)
|
||||
{
|
||||
if (condition == 0.0f) { return; }
|
||||
if (condition <= 0.0f) { return; }
|
||||
|
||||
bool remove = false;
|
||||
|
||||
@@ -2742,7 +2757,7 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
ic.PlaySound(ActionType.OnSecondaryUse, character);
|
||||
#endif
|
||||
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character);
|
||||
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character);
|
||||
|
||||
if (ic.DeleteOnUse) { remove = true; }
|
||||
}
|
||||
@@ -2761,6 +2776,13 @@ namespace Barotrauma
|
||||
if (!UseInHealthInterface) { return; }
|
||||
|
||||
#if CLIENT
|
||||
if (user == Character.Controlled)
|
||||
{
|
||||
if (HealingCooldown.IsOnCooldown) { return; }
|
||||
|
||||
HealingCooldown.PutOnCooldown();
|
||||
}
|
||||
|
||||
if (GameMain.Client != null)
|
||||
{
|
||||
GameMain.Client.CreateEntityEvent(this, new TreatmentEventData(character, targetLimb));
|
||||
@@ -2781,13 +2803,13 @@ namespace Barotrauma
|
||||
#endif
|
||||
ic.WasUsed = true;
|
||||
|
||||
ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: user);
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user);
|
||||
ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: user);
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: character, user: user);
|
||||
|
||||
if (GameMain.NetworkMember is { IsServer: true })
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb));
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb));
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb, useTarget: character));
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb, useTarget: character));
|
||||
}
|
||||
|
||||
if (ic.DeleteOnUse) { remove = true; }
|
||||
@@ -3448,7 +3470,7 @@ namespace Barotrauma
|
||||
item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition);
|
||||
}
|
||||
}
|
||||
item.lastSentCondition = item.condition;
|
||||
item.lastSentCondition = item.prevCondition = item.condition;
|
||||
item.RecalculateConditionValues();
|
||||
item.SetActiveSprite();
|
||||
|
||||
@@ -3522,15 +3544,10 @@ namespace Barotrauma
|
||||
upgrade.Save(element);
|
||||
}
|
||||
|
||||
if (condition < MaxCondition)
|
||||
{
|
||||
element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var conditionAttribute = element.GetAttribute("condition");
|
||||
if (conditionAttribute != null) { conditionAttribute.Remove(); }
|
||||
}
|
||||
element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture)));
|
||||
|
||||
var conditionAttribute = element.GetAttribute("condition");
|
||||
if (conditionAttribute != null) { conditionAttribute.Remove(); }
|
||||
|
||||
parentElement.Add(element);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Barotrauma
|
||||
public EventType EventType { get; }
|
||||
}
|
||||
|
||||
public struct ComponentStateEventData : IEventData
|
||||
public readonly struct ComponentStateEventData : IEventData
|
||||
{
|
||||
public EventType EventType => EventType.ComponentState;
|
||||
public readonly ItemComponent Component;
|
||||
|
||||
@@ -568,9 +568,11 @@ namespace Barotrauma
|
||||
|
||||
public ImmutableDictionary<Identifier, FixedQuantityResourceInfo> LevelQuantity { get; private set; }
|
||||
|
||||
public bool CanSpriteFlipX { get; private set; }
|
||||
private bool canSpriteFlipX;
|
||||
public override bool CanSpriteFlipX => canSpriteFlipX;
|
||||
|
||||
public bool CanSpriteFlipY { get; private set; }
|
||||
private bool canSpriteFlipY;
|
||||
public override bool CanSpriteFlipY => canSpriteFlipY;
|
||||
|
||||
/// <summary>
|
||||
/// Can the item be chosen as extra cargo in multiplayer. If not set, the item is available if it can be bought from outposts in the campaign.
|
||||
@@ -767,6 +769,9 @@ namespace Barotrauma
|
||||
[Serialize(true, IsPropertySaveable.No)]
|
||||
public bool ShowHealthBar { get; private set; }
|
||||
|
||||
[Serialize(1f, IsPropertySaveable.No, description: "How much the bots prioritize this item when they seek for items. For example, bots prioritize less exosuit than the other diving suits. Defaults to 1. Note that there's also a specific CombatPriority for items that can be used as weapons.")]
|
||||
public float BotPriority { get; private set; }
|
||||
|
||||
protected override Identifier DetermineIdentifier(XElement element)
|
||||
{
|
||||
Identifier identifier = base.DetermineIdentifier(element);
|
||||
@@ -884,8 +889,8 @@ namespace Barotrauma
|
||||
case "sprite":
|
||||
string spriteFolder = GetTexturePath(subElement, variantOf);
|
||||
|
||||
CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
|
||||
CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
|
||||
canSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
|
||||
canSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
|
||||
|
||||
sprite = new Sprite(subElement, spriteFolder, lazyLoad: true);
|
||||
if (subElement.GetAttribute("sourcerect") == null &&
|
||||
@@ -936,14 +941,20 @@ namespace Barotrauma
|
||||
AllowDeconstruct = true;
|
||||
RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false);
|
||||
RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1);
|
||||
foreach (XElement deconstructItem in subElement.Elements())
|
||||
foreach (XElement itemElement in subElement.Elements())
|
||||
{
|
||||
if (deconstructItem.Attribute("name") != null)
|
||||
if (itemElement.Attribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items.");
|
||||
continue;
|
||||
}
|
||||
deconstructItems.Add(new DeconstructItem(deconstructItem, Identifier));
|
||||
var deconstructItem = new DeconstructItem(itemElement, Identifier);
|
||||
if (deconstructItem.ItemIdentifier.IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier.");
|
||||
continue;
|
||||
}
|
||||
deconstructItems.Add(deconstructItem);
|
||||
}
|
||||
RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, deconstructItems.Count);
|
||||
break;
|
||||
|
||||
@@ -18,9 +18,9 @@ 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
|
||||
public const ushort MaxEntityCount = ushort.MaxValue - 4; //ushort.MaxValue - 4 because the 4 values above are reserved values
|
||||
|
||||
private static Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
|
||||
private static readonly Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
|
||||
public static IReadOnlyCollection<Entity> GetEntities()
|
||||
{
|
||||
return dictionary.Values;
|
||||
@@ -85,6 +85,28 @@ namespace Barotrauma
|
||||
this.Submarine = submarine;
|
||||
spawnTime = Timing.TotalTime;
|
||||
|
||||
if (dictionary.Count >= MaxEntityCount)
|
||||
{
|
||||
Dictionary<Identifier, int> entityCounts = new Dictionary<Identifier, int>();
|
||||
foreach (var entity in dictionary)
|
||||
{
|
||||
if (entity.Value is MapEntity me)
|
||||
{
|
||||
if (entityCounts.ContainsKey(me.Prefab.Identifier))
|
||||
{
|
||||
entityCounts[me.Prefab.Identifier]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
entityCounts[me.Prefab.Identifier] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
string errorMsg = $"Maximum amount of entities ({MaxEntityCount}) exceeded! Largest numbers of entities: " +
|
||||
string.Join(", ", entityCounts.OrderByDescending(kvp => kvp.Value).Take(10).Select(kvp => $"{kvp.Key}: {kvp.Value}"));
|
||||
throw new Exception(errorMsg);
|
||||
}
|
||||
|
||||
//give a unique ID
|
||||
ID = DetermineID(id, submarine);
|
||||
|
||||
|
||||
@@ -188,6 +188,12 @@ namespace Barotrauma
|
||||
item.Condition -= item.MaxCondition * EmpStrength * distFactor;
|
||||
}
|
||||
|
||||
var lightComponent = item.GetComponent<LightComponent>();
|
||||
if (lightComponent != null)
|
||||
{
|
||||
lightComponent.TemporaryFlickerTimer = Math.Min(EmpStrength * distFactor, 10.0f);
|
||||
}
|
||||
|
||||
//discharge batteries
|
||||
var powerContainer = item.GetComponent<PowerContainer>();
|
||||
if (powerContainer != null)
|
||||
|
||||
@@ -50,6 +50,12 @@ namespace Barotrauma
|
||||
Description = TextManager.Get($"EntityDescription.{Identifier}");
|
||||
Tags = Enumerable.Empty<Identifier>().ToImmutableHashSet();
|
||||
|
||||
string description = element.GetAttributeString("description", string.Empty);
|
||||
if (!description.IsNullOrEmpty())
|
||||
{
|
||||
Description = Description.Fallback(description);
|
||||
}
|
||||
|
||||
List<ushort> containedItemIDs = new List<ushort>();
|
||||
foreach (XElement entityElement in element.Elements())
|
||||
{
|
||||
|
||||
@@ -860,6 +860,7 @@ namespace Barotrauma
|
||||
(endPath != null && GetDistToTunnel(cell.Center, endPath) < minMainPathWidth) ||
|
||||
(endHole != null && GetDistToTunnel(cell.Center, endHole) < minMainPathWidth)) { continue; }
|
||||
if (cell.Edges.Any(e => e.AdjacentCell(cell)?.CellType != CellType.Path || e.NextToCave)) { continue; }
|
||||
if (PositionsOfInterest.Any(p => cell.IsPointInside(p.Position.ToVector2()))) { continue; }
|
||||
potentialIslands.Add(cell);
|
||||
}
|
||||
for (int i = 0; i < GenerationParams.IslandCount; i++)
|
||||
@@ -1099,6 +1100,7 @@ namespace Barotrauma
|
||||
caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells));
|
||||
foreach (var caveCell in caveCells)
|
||||
{
|
||||
if (PositionsOfInterest.Any(p => caveCell.IsPointInside(p.Position.ToVector2()))) { continue; }
|
||||
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio)
|
||||
{
|
||||
var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f);
|
||||
@@ -3176,7 +3178,7 @@ namespace Barotrauma
|
||||
TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter);
|
||||
|
||||
Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient);
|
||||
if (!cells.Any(c => c.IsPointInside(startPos + offset)))
|
||||
if (!IsPositionInsideWall(startPos + offset))
|
||||
{
|
||||
startPos += offset;
|
||||
}
|
||||
@@ -3232,10 +3234,9 @@ namespace Barotrauma
|
||||
{
|
||||
suitablePositions.RemoveAll(p => !filter(p));
|
||||
}
|
||||
//avoid floating ice chunks on the main path
|
||||
if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath))
|
||||
{
|
||||
suitablePositions.RemoveAll(p => ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(p.Position.ToVector2()))));
|
||||
suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2()));
|
||||
}
|
||||
if (!suitablePositions.Any())
|
||||
{
|
||||
@@ -3288,10 +3289,16 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position;
|
||||
position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)].Position;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsPositionInsideWall(Vector2 worldPosition)
|
||||
{
|
||||
var closestCell = GetClosestCell(worldPosition);
|
||||
return closestCell != null && closestCell.IsPointInside(worldPosition);
|
||||
}
|
||||
|
||||
public void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
LevelObjectManager.Update(deltaTime);
|
||||
@@ -3334,14 +3341,13 @@ namespace Barotrauma
|
||||
|
||||
public Vector2 GetBottomPosition(float xPosition)
|
||||
{
|
||||
int index = (int)Math.Floor(xPosition / Size.X * (bottomPositions.Count - 1));
|
||||
float interval = Size.X / (bottomPositions.Count - 1);
|
||||
|
||||
int index = (int)Math.Floor(xPosition / interval);
|
||||
if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); }
|
||||
|
||||
float t = (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X);
|
||||
//t can go slightly outside the 0-1 due to rounding, safe to ignore
|
||||
Debug.Assert(t <= 1.001f && t >= -0.001f);
|
||||
float t = (xPosition - bottomPositions[index].X) / interval;
|
||||
t = MathHelper.Clamp(t, 0.0f, 1.0f);
|
||||
|
||||
float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t);
|
||||
|
||||
return new Vector2(xPosition, yPos);
|
||||
|
||||
@@ -112,10 +112,9 @@ namespace Barotrauma
|
||||
(int)MathUtils.Round(generationParams.Height, Level.GridCellSize));
|
||||
}
|
||||
|
||||
public LevelData(XElement element, float? forceDifficulty = null)
|
||||
public LevelData(XElement element, float? forceDifficulty = null, bool clampDifficultyToBiome = false)
|
||||
{
|
||||
Seed = element.GetAttributeString("seed", "");
|
||||
Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f);
|
||||
Size = element.GetAttributePoint("size", new Point(1000));
|
||||
Enum.TryParse(element.GetAttributeString("type", "LocationConnection"), out Type);
|
||||
|
||||
@@ -131,10 +130,7 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.ThrowError($"Error while loading a level. Could not find level generation params with the ID \"{generationParamsId}\".");
|
||||
GenerationParams = LevelGenerationParams.LevelParams.FirstOrDefault(l => l.Type == Type);
|
||||
if (GenerationParams == null)
|
||||
{
|
||||
GenerationParams = LevelGenerationParams.LevelParams.First();
|
||||
}
|
||||
GenerationParams ??= LevelGenerationParams.LevelParams.First();
|
||||
}
|
||||
|
||||
InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin);
|
||||
@@ -147,10 +143,16 @@ namespace Barotrauma
|
||||
Biome = Biome.Prefabs.First();
|
||||
}
|
||||
|
||||
string[] prefabNames = element.GetAttributeStringArray("eventhistory", new string[] { });
|
||||
Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f);
|
||||
if (clampDifficultyToBiome)
|
||||
{
|
||||
Difficulty = MathHelper.Clamp(Difficulty, Biome.MinDifficulty, Biome.AdjustedMaxDifficulty);
|
||||
}
|
||||
|
||||
string[] prefabNames = element.GetAttributeStringArray("eventhistory", Array.Empty<string>());
|
||||
EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n)));
|
||||
|
||||
string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { });
|
||||
string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", Array.Empty<string>());
|
||||
NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)));
|
||||
|
||||
EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false);
|
||||
|
||||
@@ -559,12 +559,9 @@ namespace Barotrauma
|
||||
killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty<int>()).ToHashSet();
|
||||
|
||||
System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!");
|
||||
if (Type == null)
|
||||
{
|
||||
Type = LocationType.Prefabs.First();
|
||||
}
|
||||
Type ??= LocationType.Prefabs.First();
|
||||
|
||||
LevelData = new LevelData(element.Element("Level"));
|
||||
LevelData = new LevelData(element.Element("Level"), clampDifficultyToBiome: true);
|
||||
|
||||
PortraitId = ToolBox.StringToInt(Name);
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
int startLocationindex = element.GetAttributeInt("startlocation", -1);
|
||||
if (startLocationindex > 0 && startLocationindex < Locations.Count)
|
||||
if (startLocationindex >= 0 && startLocationindex < Locations.Count)
|
||||
{
|
||||
StartLocation = Locations[startLocationindex];
|
||||
}
|
||||
@@ -188,7 +188,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
int endLocationindex = element.GetAttributeInt("endlocation", -1);
|
||||
if (endLocationindex > 0 && endLocationindex < Locations.Count)
|
||||
if (endLocationindex >= 0 && endLocationindex < Locations.Count)
|
||||
{
|
||||
EndLocation = Locations[endLocationindex];
|
||||
}
|
||||
|
||||
@@ -174,6 +174,9 @@ namespace Barotrauma
|
||||
|
||||
public abstract Sprite Sprite { get; }
|
||||
|
||||
public virtual bool CanSpriteFlipX { get; } = false;
|
||||
public virtual bool CanSpriteFlipY { get; } = false;
|
||||
|
||||
public abstract string OriginalName { get; }
|
||||
|
||||
public abstract LocalizedString Name { get; }
|
||||
|
||||
@@ -124,9 +124,18 @@ namespace Barotrauma
|
||||
int eventCount = GameMain.Server.EntityEventManager.Events.Count();
|
||||
int uniqueEventCount = GameMain.Server.EntityEventManager.UniqueEvents.Count();
|
||||
#endif
|
||||
List<MapEntity> entities = MapEntity.mapEntityList.FindAll(e => e.Submarine == sub);
|
||||
HashSet<Submarine> connectedSubs = new HashSet<Submarine>() { sub };
|
||||
foreach (Submarine otherSub in Submarine.Loaded)
|
||||
{
|
||||
//remove linked subs too
|
||||
if (otherSub.Submarine == sub) { connectedSubs.Add(otherSub); }
|
||||
}
|
||||
List<MapEntity> entities = MapEntity.mapEntityList.FindAll(e => connectedSubs.Contains(e.Submarine));
|
||||
entities.ForEach(e => e.Remove());
|
||||
sub.Remove();
|
||||
foreach (Submarine otherSub in connectedSubs)
|
||||
{
|
||||
otherSub.Remove();
|
||||
}
|
||||
#if SERVER
|
||||
//remove any events created during the removal of the entities
|
||||
GameMain.Server.EntityEventManager.Events.RemoveRange(eventCount, GameMain.Server.EntityEventManager.Events.Count - eventCount);
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace Barotrauma
|
||||
|
||||
public readonly ContentXElement ConfigElement;
|
||||
|
||||
public readonly bool CanSpriteFlipX;
|
||||
public readonly bool CanSpriteFlipY;
|
||||
public override bool CanSpriteFlipX { get; }
|
||||
public override bool CanSpriteFlipY { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If null, the orientation is determined automatically based on the dimensions of the structure instances
|
||||
|
||||
@@ -1310,7 +1310,7 @@ namespace Barotrauma
|
||||
return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W));
|
||||
}
|
||||
|
||||
public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
|
||||
public Submarine(SubmarineInfo info, bool showErrorMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
|
||||
{
|
||||
upgradeEventIdentifier = new Identifier($"Submarine{ID}");
|
||||
Loading = true;
|
||||
@@ -1381,65 +1381,65 @@ namespace Barotrauma
|
||||
center.Y -= center.Y % GridSize.Y;
|
||||
|
||||
RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this));
|
||||
}
|
||||
|
||||
subBody = new SubmarineBody(this, showWarningMessages);
|
||||
Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition);
|
||||
subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f);
|
||||
subBody = new SubmarineBody(this, showErrorMessages);
|
||||
Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition);
|
||||
subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f);
|
||||
|
||||
if (info.IsOutpost)
|
||||
if (info.IsOutpost)
|
||||
{
|
||||
ShowSonarMarker = false;
|
||||
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
|
||||
TeamID = CharacterTeamType.FriendlyNPC;
|
||||
|
||||
bool indestructible =
|
||||
GameMain.NetworkMember != null &&
|
||||
!GameMain.NetworkMember.ServerSettings.DestructibleOutposts &&
|
||||
!(info.OutpostGenerationParams?.AlwaysDestructible ?? false);
|
||||
|
||||
foreach (MapEntity me in MapEntity.mapEntityList)
|
||||
{
|
||||
ShowSonarMarker = false;
|
||||
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
|
||||
TeamID = CharacterTeamType.FriendlyNPC;
|
||||
|
||||
bool indestructible =
|
||||
GameMain.NetworkMember != null &&
|
||||
!GameMain.NetworkMember.ServerSettings.DestructibleOutposts &&
|
||||
!(info.OutpostGenerationParams?.AlwaysDestructible ?? false);
|
||||
|
||||
foreach (MapEntity me in MapEntity.mapEntityList)
|
||||
if (me.Submarine != this) { continue; }
|
||||
if (me is Item item)
|
||||
{
|
||||
if (me.Submarine != this) { continue; }
|
||||
if (me is Item item)
|
||||
item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null;
|
||||
item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true;
|
||||
if (item.GetComponent<Repairable>() != null && indestructible)
|
||||
{
|
||||
item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null;
|
||||
item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true;
|
||||
if (item.GetComponent<Repairable>() != null && indestructible)
|
||||
item.Indestructible = true;
|
||||
}
|
||||
foreach (ItemComponent ic in item.Components)
|
||||
{
|
||||
if (ic is ConnectionPanel connectionPanel)
|
||||
{
|
||||
item.Indestructible = true;
|
||||
}
|
||||
foreach (ItemComponent ic in item.Components)
|
||||
{
|
||||
if (ic is ConnectionPanel connectionPanel)
|
||||
//prevent rewiring
|
||||
if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable)
|
||||
{
|
||||
//prevent rewiring
|
||||
if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable)
|
||||
{
|
||||
connectionPanel.Locked = true;
|
||||
}
|
||||
connectionPanel.Locked = true;
|
||||
}
|
||||
else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
|
||||
{
|
||||
//prevent deattaching items from walls
|
||||
}
|
||||
else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
|
||||
{
|
||||
//prevent deattaching items from walls
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession?.GameMode is TutorialMode) { continue; }
|
||||
#endif
|
||||
holdable.CanBePicked = false;
|
||||
holdable.CanBeSelected = false;
|
||||
}
|
||||
holdable.CanBePicked = false;
|
||||
holdable.CanBeSelected = false;
|
||||
}
|
||||
}
|
||||
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible)
|
||||
{
|
||||
structure.Indestructible = true;
|
||||
}
|
||||
}
|
||||
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible)
|
||||
{
|
||||
structure.Indestructible = true;
|
||||
}
|
||||
}
|
||||
else if (info.IsRuin)
|
||||
{
|
||||
ShowSonarMarker = false;
|
||||
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
|
||||
}
|
||||
}
|
||||
else if (info.IsRuin)
|
||||
{
|
||||
ShowSonarMarker = false;
|
||||
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
|
||||
}
|
||||
|
||||
if (entityGrid != null)
|
||||
@@ -1481,7 +1481,7 @@ namespace Barotrauma
|
||||
#endif
|
||||
//if the sub was made using an older version,
|
||||
//halve the brightness of the lights to make them look (almost) right on the new lighting formula
|
||||
if (showWarningMessages &&
|
||||
if (showErrorMessages &&
|
||||
!string.IsNullOrEmpty(Info.FilePath) &&
|
||||
Screen.Selected != GameMain.SubEditorScreen &&
|
||||
(Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0")))
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Barotrauma
|
||||
get { return submarine; }
|
||||
}
|
||||
|
||||
public SubmarineBody(Submarine sub, bool showWarningMessages = true)
|
||||
public SubmarineBody(Submarine sub, bool showErrorMessages = true)
|
||||
{
|
||||
this.submarine = sub;
|
||||
|
||||
@@ -119,9 +119,9 @@ namespace Barotrauma
|
||||
if (!Hull.HullList.Any(h => h.Submarine == sub))
|
||||
{
|
||||
farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f);
|
||||
if (showWarningMessages)
|
||||
if (showErrorMessages)
|
||||
{
|
||||
DebugConsole.ThrowError("WARNING: no hulls found, generating a physics body for the submarine failed.");
|
||||
DebugConsole.ThrowError($"No hulls found in the submarine \"{sub.Info.Name}\". Generating a physics body for the submarine failed.");
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -531,11 +531,15 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Calculated from <see cref="SubmarineElement"/>. Can be used when the sub hasn't been loaded and we can't access <see cref="Submarine.RealWorldCrushDepth"/>.
|
||||
/// </summary>
|
||||
public float GetRealWorldCrushDepth()
|
||||
public bool IsCrushDepthDefinedInStructures(out float realWorldCrushDepth)
|
||||
{
|
||||
if (SubmarineElement == null) { return Level.DefaultRealWorldCrushDepth; }
|
||||
if (SubmarineElement == null)
|
||||
{
|
||||
realWorldCrushDepth = Level.DefaultRealWorldCrushDepth;
|
||||
return false;
|
||||
}
|
||||
bool structureCrushDepthsDefined = false;
|
||||
float realWorldCrushDepth = float.PositiveInfinity;
|
||||
realWorldCrushDepth = float.PositiveInfinity;
|
||||
foreach (var structureElement in SubmarineElement.GetChildElements("structure"))
|
||||
{
|
||||
string name = structureElement.Attribute("name")?.Value ?? "";
|
||||
@@ -553,7 +557,7 @@ namespace Barotrauma
|
||||
{
|
||||
realWorldCrushDepth = Level.DefaultRealWorldCrushDepth;
|
||||
}
|
||||
return realWorldCrushDepth;
|
||||
return structureCrushDepthsDefined;
|
||||
}
|
||||
|
||||
//saving/loading ----------------------------------------------------
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Barotrauma.Networking
|
||||
public readonly Either<Address, AccountId> AddressOrAccountId;
|
||||
|
||||
public readonly string Reason;
|
||||
public DateTime? ExpirationTime;
|
||||
public Option<SerializableDateTime> ExpirationTime;
|
||||
public readonly UInt32 UniqueIdentifier;
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
public T GetVote<T>(VoteType voteType)
|
||||
{
|
||||
return (votes[(int)voteType] is T) ? (T)votes[(int)voteType] : default(T);
|
||||
return (votes[(int)voteType] is T t) ? t : default;
|
||||
}
|
||||
|
||||
public void SetVote(VoteType voteType, object value)
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
interface IServerPositionSync : IServerSerializable
|
||||
{
|
||||
#if SERVER
|
||||
void ServerWritePosition(IWriteMessage msg, Client c);
|
||||
void ServerWritePosition(ReadWriteMessage tempBuffer, Client c);
|
||||
#endif
|
||||
#if CLIENT
|
||||
void ClientReadPosition(IReadMessage msg, float sendingTime);
|
||||
|
||||
@@ -160,7 +160,8 @@ namespace Barotrauma
|
||||
{ typeof(Identifier), new ReadWriteBehavior<Identifier>(ReadIdentifier, WriteIdentifier) },
|
||||
{ typeof(AccountId), new ReadWriteBehavior<AccountId>(ReadAccountId, WriteAccountId) },
|
||||
{ typeof(Color), new ReadWriteBehavior<Color>(ReadColor, WriteColor) },
|
||||
{ typeof(Vector2), new ReadWriteBehavior<Vector2>(ReadVector2, WriteVector2) }
|
||||
{ typeof(Vector2), new ReadWriteBehavior<Vector2>(ReadVector2, WriteVector2) },
|
||||
{ typeof(SerializableDateTime), new ReadWriteBehavior<SerializableDateTime>(ReadSerializableDateTime, WriteSerializableDateTime) }
|
||||
};
|
||||
|
||||
private static readonly ImmutableDictionary<Predicate<Type>, Func<Type, IReadWriteBehavior>> BehaviorFactories = new Dictionary<Predicate<Type>, Func<Type, IReadWriteBehavior>>
|
||||
@@ -512,6 +513,41 @@ namespace Barotrauma
|
||||
WriteSingle(y, attribute, msg, bitField);
|
||||
}
|
||||
|
||||
private static readonly Range<Int64> ValidTickRange
|
||||
= new Range<Int64>(
|
||||
start: DateTime.MinValue.Ticks,
|
||||
end: DateTime.MaxValue.Ticks);
|
||||
private static readonly Range<Int16> ValidTimeZoneMinuteRange
|
||||
= new Range<Int16>(
|
||||
start: (Int16)TimeSpan.FromHours(-12).TotalMinutes,
|
||||
end: (Int16)TimeSpan.FromHours(14).TotalMinutes);
|
||||
|
||||
private static SerializableDateTime ReadSerializableDateTime(
|
||||
IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField)
|
||||
{
|
||||
var ticks = inc.ReadInt64();
|
||||
var timezone = inc.ReadInt16();
|
||||
|
||||
if (!ValidTickRange.Contains(ticks))
|
||||
{
|
||||
throw new Exception($"Incoming SerializableDateTime ticks out of range (ticks: {ticks}, timezone: {timezone})");
|
||||
}
|
||||
if (!ValidTimeZoneMinuteRange.Contains(timezone))
|
||||
{
|
||||
throw new Exception($"Incoming SerializableDateTime timezone out of range (ticks: {ticks}, timezone: {timezone})");
|
||||
}
|
||||
|
||||
return new SerializableDateTime(new DateTime(ticks),
|
||||
new SerializableTimeZone(TimeSpan.FromMinutes(timezone)));
|
||||
}
|
||||
|
||||
private static void WriteSerializableDateTime(
|
||||
SerializableDateTime dateTime, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField)
|
||||
{
|
||||
msg.WriteInt64(dateTime.Ticks);
|
||||
msg.WriteInt16((Int16)(dateTime.TimeZone.Value.Ticks / TimeSpan.TicksPerMinute));
|
||||
}
|
||||
|
||||
private static bool IsRanged(float minValue, float maxValue) => minValue > float.MinValue || maxValue < float.MaxValue;
|
||||
private static bool IsRanged(int minValue, int maxValue) => minValue > int.MinValue || maxValue < int.MaxValue;
|
||||
|
||||
|
||||
@@ -99,6 +99,19 @@ namespace Barotrauma.Networking
|
||||
EntityEventInitial
|
||||
}
|
||||
|
||||
[NetworkSerialize]
|
||||
readonly record struct EntityPositionHeader(
|
||||
bool IsItem,
|
||||
UInt32 PrefabUintIdentifier,
|
||||
UInt16 EntityId) : INetSerializableStruct
|
||||
{
|
||||
public static EntityPositionHeader FromEntity(Entity entity)
|
||||
=> new (
|
||||
IsItem: entity is Item,
|
||||
PrefabUintIdentifier: entity is MapEntity me ? me.Prefab.UintIdentifier : 0,
|
||||
EntityId: entity.ID);
|
||||
}
|
||||
|
||||
enum TraitorMessageType
|
||||
{
|
||||
Server,
|
||||
|
||||
+3
-1
@@ -171,6 +171,8 @@ namespace Barotrauma.Networking
|
||||
public bool ShouldCreateAnalyticsEvent
|
||||
=> DisconnectReason is not (
|
||||
DisconnectReason.Disconnected
|
||||
or DisconnectReason.ServerShutdown
|
||||
or DisconnectReason.ServerFull
|
||||
or DisconnectReason.Banned
|
||||
or DisconnectReason.Kicked
|
||||
or DisconnectReason.TooManyFailedLogins
|
||||
@@ -300,7 +302,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
public ServerContentPackage() { }
|
||||
|
||||
public ServerContentPackage(ContentPackage contentPackage, DateTime referenceTime)
|
||||
public ServerContentPackage(ContentPackage contentPackage, SerializableDateTime referenceTime)
|
||||
{
|
||||
Name = contentPackage.Name;
|
||||
Hash = contentPackage.Hash;
|
||||
|
||||
@@ -1093,7 +1093,7 @@ namespace Barotrauma.Networking
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int index = msg.ReadUInt16();
|
||||
if (index < 0 || index >= subList.Count) { continue; }
|
||||
if (index >= subList.Count) { continue; }
|
||||
string submarineName = subList[index].Name;
|
||||
HiddenSubs.Add(submarineName);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
public enum VoteState { None = 0, Started = 1, Running = 2, Passed = 3, Failed = 4 };
|
||||
|
||||
private IReadOnlyDictionary<T, int> GetVoteCounts<T>(VoteType voteType, IEnumerable<Client> voters)
|
||||
private static IReadOnlyDictionary<T, int> GetVoteCounts<T>(VoteType voteType, IEnumerable<Client> voters)
|
||||
{
|
||||
Dictionary<T, int> voteList = new Dictionary<T, int>();
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Barotrauma
|
||||
return voteList;
|
||||
}
|
||||
|
||||
public T HighestVoted<T>(VoteType voteType, List<Client> voters)
|
||||
public static T HighestVoted<T>(VoteType voteType, IEnumerable<Client> voters)
|
||||
{
|
||||
if (voteType == VoteType.Sub && !GameMain.NetworkMember.ServerSettings.AllowSubVoting) { return default; }
|
||||
if (voteType == VoteType.Mode && !GameMain.NetworkMember.ServerSettings.AllowModeVoting) { return default; }
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
using System;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using File = Barotrauma.IO.File;
|
||||
using FileStream = Barotrauma.IO.FileStream;
|
||||
using Path = Barotrauma.IO.Path;
|
||||
@@ -315,7 +314,7 @@ namespace Barotrauma
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
|
||||
LogAttributeError(attribute, element, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +356,7 @@ namespace Barotrauma
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
|
||||
LogAttributeError(attribute, element, e);
|
||||
}
|
||||
|
||||
return val;
|
||||
@@ -376,7 +375,7 @@ namespace Barotrauma
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
|
||||
LogAttributeError(attribute, element, e);
|
||||
}
|
||||
|
||||
return val;
|
||||
@@ -395,12 +394,22 @@ namespace Barotrauma
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
|
||||
LogAttributeError(attribute, element, e);
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
public static Option<SerializableDateTime> GetAttributeDateTime(
|
||||
this XElement element, string name)
|
||||
{
|
||||
var attribute = element?.GetAttribute(name);
|
||||
if (attribute == null) { return Option<SerializableDateTime>.None(); }
|
||||
|
||||
string attrVal = attribute.Value;
|
||||
return SerializableDateTime.Parse(attrVal);
|
||||
}
|
||||
|
||||
public static Version GetAttributeVersion(this XElement element, string name, Version defaultValue)
|
||||
{
|
||||
var attribute = element?.GetAttribute(name);
|
||||
@@ -414,7 +423,7 @@ namespace Barotrauma
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
|
||||
LogAttributeError(attribute, element, e);
|
||||
}
|
||||
|
||||
return val;
|
||||
@@ -439,7 +448,7 @@ namespace Barotrauma
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
|
||||
LogAttributeError(attribute, element, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +473,7 @@ namespace Barotrauma
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
|
||||
LogAttributeError(attribute, element, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,13 +575,26 @@ namespace Barotrauma
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
|
||||
LogAttributeError(attribute, element, e);
|
||||
}
|
||||
}
|
||||
|
||||
return colorValue;
|
||||
}
|
||||
|
||||
private static void LogAttributeError(XAttribute attribute, XElement element, Exception e)
|
||||
{
|
||||
string elementStr = element.ToString();
|
||||
if (elementStr.Length > 500)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error when reading attribute \"{attribute}\"!", e);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Error when reading attribute \"{attribute.Name}\" from {elementStr}!", e);
|
||||
}
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
public static KeyOrMouse GetAttributeKeyOrMouse(this XElement element, string name, KeyOrMouse defaultValue)
|
||||
{
|
||||
@@ -621,6 +643,15 @@ namespace Barotrauma
|
||||
return stringValue.Split(';').Select(s => ParseTuple<T1, T2>(s, default)).ToArray();
|
||||
}
|
||||
|
||||
public static Range<int> GetAttributeRange(this XElement element, string name, Range<int> defaultValue)
|
||||
{
|
||||
var attribute = element?.GetAttribute(name);
|
||||
if (attribute is null) { return defaultValue; }
|
||||
|
||||
string stringValue = attribute.Value;
|
||||
return string.IsNullOrEmpty(stringValue) ? defaultValue : ParseRange(stringValue);
|
||||
}
|
||||
|
||||
public static string ElementInnerText(this XElement el)
|
||||
{
|
||||
StringBuilder str = new StringBuilder();
|
||||
@@ -895,6 +926,37 @@ namespace Barotrauma
|
||||
return floatArray;
|
||||
}
|
||||
|
||||
// parse a range string, e.g "1-3" or "3"
|
||||
public static Range<int> ParseRange(string rangeString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rangeString)) { return GetDefault(rangeString); }
|
||||
|
||||
string[] split = rangeString.Split('-');
|
||||
return split.Length switch
|
||||
{
|
||||
1 when TryParseInt(split[0], out int value) => new Range<int>(value, value),
|
||||
2 when TryParseInt(split[0], out int min) && TryParseInt(split[1], out int max) && min < max => new Range<int>(min, max),
|
||||
_ => GetDefault(rangeString)
|
||||
};
|
||||
|
||||
static bool TryParseInt(string value, out int result)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result);
|
||||
}
|
||||
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
static Range<int> GetDefault(string rangeString)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error parsing range: \"{rangeString}\" (using default value 0-99)");
|
||||
return new Range<int>(0, 99);
|
||||
}
|
||||
}
|
||||
|
||||
public static Identifier VariantOf(this XElement element) =>
|
||||
element.GetAttributeIdentifier("inherit", element.GetAttributeIdentifier("variantof", ""));
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ namespace Barotrauma
|
||||
EnableMouseLook = true,
|
||||
ChatOpen = true,
|
||||
CrewMenuOpen = true,
|
||||
EditorDisclaimerShown = false,
|
||||
ShowOffensiveServerPrompt = true,
|
||||
TutorialSkipWarning = true,
|
||||
CorpseDespawnDelay = 600,
|
||||
@@ -132,7 +131,6 @@ namespace Barotrauma
|
||||
public EnemyHealthBarMode ShowEnemyHealthBars;
|
||||
public bool ChatOpen;
|
||||
public bool CrewMenuOpen;
|
||||
public bool EditorDisclaimerShown;
|
||||
public bool ShowOffensiveServerPrompt;
|
||||
public bool TutorialSkipWarning;
|
||||
public int CorpseDespawnDelay;
|
||||
|
||||
@@ -75,6 +75,7 @@ namespace Barotrauma
|
||||
case "targetgrandparent":
|
||||
case "targetcontaineditem":
|
||||
case "skillrequirement":
|
||||
case "targetslot":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
|
||||
@@ -1455,7 +1455,7 @@ namespace Barotrauma
|
||||
if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; }
|
||||
AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue);
|
||||
limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source);
|
||||
RegisterTreatmentResults(entity, limb, affliction, result);
|
||||
RegisterTreatmentResults(user, entity as Item, limb, affliction, result);
|
||||
//only apply non-limb-specific afflictions to the first limb
|
||||
if (!affliction.Prefab.LimbSpecific) { break; }
|
||||
}
|
||||
@@ -1467,7 +1467,7 @@ namespace Barotrauma
|
||||
newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality);
|
||||
AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue);
|
||||
limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source);
|
||||
RegisterTreatmentResults(entity, limb, affliction, result);
|
||||
RegisterTreatmentResults(user, entity as Item, limb, affliction, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1498,17 +1498,18 @@ namespace Barotrauma
|
||||
{
|
||||
targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType);
|
||||
}
|
||||
targetCharacter.AIController?.OnHealed(healer: user, targetCharacter.Vitality - prevVitality);
|
||||
if (user != null && user != targetCharacter)
|
||||
if (!targetCharacter.IsDead)
|
||||
{
|
||||
if (!targetCharacter.IsDead)
|
||||
float healthChange = targetCharacter.Vitality - prevVitality;
|
||||
targetCharacter.AIController?.OnHealed(healer: user, healthChange);
|
||||
if (user != null)
|
||||
{
|
||||
targetCharacter.TryAdjustAttackerSkill(user, targetCharacter.Vitality - prevVitality);
|
||||
}
|
||||
};
|
||||
targetCharacter.TryAdjustHealerSkill(user, healthChange);
|
||||
#if SERVER
|
||||
GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, prevVitality - targetCharacter.Vitality, 0.0f);
|
||||
GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, healthChange, 0.0f);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1760,12 +1761,17 @@ namespace Barotrauma
|
||||
|
||||
void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo)
|
||||
{
|
||||
Item parentItem = entity as Item;
|
||||
if (user == null && parentItem != null)
|
||||
{
|
||||
// Set the user for projectiles spawned from status effects (e.g. flak shrapnels)
|
||||
SetUser(parentItem.GetComponent<Projectile>()?.User);
|
||||
}
|
||||
switch (chosenItemSpawnInfo.SpawnPosition)
|
||||
{
|
||||
case ItemSpawnInfo.SpawnPositionType.This:
|
||||
Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem =>
|
||||
{
|
||||
Item parentItem = entity as Item;
|
||||
Projectile projectile = newItem.GetComponent<Projectile>();
|
||||
if (entity != null)
|
||||
{
|
||||
@@ -2068,14 +2074,14 @@ namespace Barotrauma
|
||||
if (character.Removed) { continue; }
|
||||
newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality);
|
||||
var result = character.AddDamage(character.WorldPosition, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attacker: element.User);
|
||||
element.Parent.RegisterTreatmentResults(element.Entity, result.HitLimb, affliction, result);
|
||||
element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, result.HitLimb, affliction, result);
|
||||
}
|
||||
else if (target is Limb limb)
|
||||
{
|
||||
if (limb.character.Removed || limb.Removed) { continue; }
|
||||
newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, limb.character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality);
|
||||
var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User);
|
||||
element.Parent.RegisterTreatmentResults(element.Entity, limb, affliction, result);
|
||||
element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, limb, affliction, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2106,17 +2112,18 @@ namespace Barotrauma
|
||||
{
|
||||
targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType);
|
||||
}
|
||||
if (element.User != null && element.User != targetCharacter)
|
||||
if (!targetCharacter.IsDead)
|
||||
{
|
||||
targetCharacter.AIController?.OnHealed(healer: element.User, targetCharacter.Vitality - prevVitality);
|
||||
if (!targetCharacter.IsDead)
|
||||
float healthChange = targetCharacter.Vitality - prevVitality;
|
||||
targetCharacter.AIController?.OnHealed(healer: element.User, healthChange);
|
||||
if (element.User != null)
|
||||
{
|
||||
targetCharacter.TryAdjustAttackerSkill(element.User, targetCharacter.Vitality - prevVitality);
|
||||
}
|
||||
};
|
||||
targetCharacter.TryAdjustHealerSkill(element.User, healthChange);
|
||||
#if SERVER
|
||||
GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, prevVitality - targetCharacter.Vitality, 0.0f);
|
||||
GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, healthChange, 0.0f);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2165,7 +2172,7 @@ namespace Barotrauma
|
||||
{
|
||||
afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemDurationMultiplier);
|
||||
}
|
||||
else if (affliction.Prefab.AfflictionType == "poison")
|
||||
else if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis")
|
||||
{
|
||||
afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier);
|
||||
}
|
||||
@@ -2178,23 +2185,25 @@ namespace Barotrauma
|
||||
return affliction;
|
||||
}
|
||||
|
||||
private void RegisterTreatmentResults(Entity entity, Limb limb, Affliction affliction, AttackResult result)
|
||||
private void RegisterTreatmentResults(Character user, Item item, Limb limb, Affliction affliction, AttackResult result)
|
||||
{
|
||||
if (entity is Item item && item.UseInHealthInterface && limb != null)
|
||||
if (item == null) { return; }
|
||||
if (!item.UseInHealthInterface) { return; }
|
||||
if (limb == null) { return; }
|
||||
foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions())
|
||||
{
|
||||
foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions())
|
||||
if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) &&
|
||||
(!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb))
|
||||
{
|
||||
if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) &&
|
||||
(!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb))
|
||||
if (type == ActionType.OnUse || type == ActionType.OnSuccess)
|
||||
{
|
||||
if (type == ActionType.OnUse || type == ActionType.OnSuccess)
|
||||
{
|
||||
limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
|
||||
}
|
||||
else if (type == ActionType.OnFailure)
|
||||
{
|
||||
limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime;
|
||||
}
|
||||
limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
|
||||
limb.character.TryAdjustHealerSkill(user, affliction: affliction);
|
||||
}
|
||||
else if (type == ActionType.OnFailure)
|
||||
{
|
||||
limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime;
|
||||
limb.character.TryAdjustHealerSkill(user, affliction: affliction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Barotrauma.Steam
|
||||
int prevSize = 0;
|
||||
for (int i = 1; i <= (maxPages ?? int.MaxValue); i++)
|
||||
{
|
||||
Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i);
|
||||
using Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i);
|
||||
if (page is not { Entries: var entries }) { break; }
|
||||
|
||||
// This queries the results on the i-th page and stores them,
|
||||
@@ -495,7 +495,8 @@ namespace Barotrauma.Steam
|
||||
new XAttribute("corepackage", isCorePackage),
|
||||
new XAttribute("modversion", modVersion),
|
||||
new XAttribute("gameversion", gameVersion),
|
||||
new XAttribute("installtime", ToolBox.Epoch.FromDateTime(updateTime)));
|
||||
#warning TODO: stop writing Unix time after this gets on main
|
||||
new XAttribute("installtime", new SerializableDateTime(updateTime).ToUnixTime()));
|
||||
if ((modPathDirName ?? modName).ToIdentifier() != itemTitle)
|
||||
{
|
||||
root.Add(new XAttribute("altnames", modPathDirName ?? modName));
|
||||
|
||||
@@ -302,6 +302,7 @@ namespace Barotrauma
|
||||
UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier());
|
||||
}
|
||||
|
||||
// TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest.
|
||||
if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null)
|
||||
{
|
||||
UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
|
||||
@@ -315,6 +316,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest.
|
||||
if (item.Prefab.Identifier == "morbusine")
|
||||
{
|
||||
UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace Barotrauma
|
||||
case int _:
|
||||
case double _:
|
||||
{
|
||||
var value = (float) OriginalValue;
|
||||
var value = Convert.ToSingle(OriginalValue);
|
||||
return level == 0 ? value : CalculateUpgrade(value, level, Multiplier);
|
||||
}
|
||||
case bool _ when bool.TryParse(Multiplier, out bool result):
|
||||
|
||||
@@ -257,10 +257,59 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class UpgradePrefab : UpgradeContentPrefab
|
||||
internal readonly struct UpgradeResourceCost
|
||||
{
|
||||
public readonly int Amount;
|
||||
private readonly ImmutableArray<Identifier> targetTags;
|
||||
public readonly Range<int> TargetLevels;
|
||||
|
||||
public UpgradeResourceCost(ContentXElement element)
|
||||
{
|
||||
Amount = element.GetAttributeInt("amount", 0);
|
||||
targetTags = element.GetAttributeIdentifierArray("item", Array.Empty<Identifier>())!.ToImmutableArray();
|
||||
TargetLevels = element.GetAttributeRange("levels", new Range<int>(0, 99));
|
||||
}
|
||||
|
||||
public bool AppliesForLevel(int currentLevel) => TargetLevels.Contains(currentLevel);
|
||||
|
||||
public bool AppliesForLevel(Range<int> newLevels) => newLevels.Start <= TargetLevels.End && newLevels.End >= TargetLevels.Start;
|
||||
|
||||
public bool MatchesItem(Item item) => MatchesItem(item.Prefab);
|
||||
|
||||
public bool MatchesItem(ItemPrefab item)
|
||||
{
|
||||
foreach (Identifier tag in targetTags)
|
||||
{
|
||||
if (tag.Equals(item.Identifier) || item.Tags.Contains(tag)) { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct ApplicableResourceCollection
|
||||
{
|
||||
public readonly ImmutableArray<ItemPrefab> MatchingItems;
|
||||
public readonly UpgradeResourceCost Cost;
|
||||
public readonly int Count;
|
||||
|
||||
public ApplicableResourceCollection(IEnumerable<ItemPrefab> matchingItems, int count, UpgradeResourceCost cost)
|
||||
{
|
||||
MatchingItems = matchingItems.ToImmutableArray();
|
||||
Count = count;
|
||||
Cost = cost;
|
||||
}
|
||||
|
||||
public static ApplicableResourceCollection CreateFor(UpgradeResourceCost cost)
|
||||
{
|
||||
return new ApplicableResourceCollection(ItemPrefab.Prefabs.Where(cost.MatchesItem), cost.Amount, cost);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class UpgradePrefab : UpgradeContentPrefab
|
||||
{
|
||||
public static readonly PrefabCollection<UpgradePrefab> Prefabs = new PrefabCollection<UpgradePrefab>(
|
||||
onAdd: (prefab, isOverride) =>
|
||||
onAdd: static (prefab, isOverride) =>
|
||||
{
|
||||
if (!prefab.SuppressWarnings && !isOverride)
|
||||
{
|
||||
@@ -329,6 +378,7 @@ namespace Barotrauma
|
||||
|
||||
private Dictionary<string, string[]> targetProperties { get; }
|
||||
private readonly ImmutableArray<UpgradeMaxLevelMod> MaxLevelsMods;
|
||||
public readonly ImmutableHashSet<UpgradeResourceCost> ResourceCosts;
|
||||
|
||||
public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file)
|
||||
{
|
||||
@@ -341,6 +391,7 @@ namespace Barotrauma
|
||||
|
||||
var targetProperties = new Dictionary<string, string[]>();
|
||||
var maxLevels = new List<UpgradeMaxLevelMod>();
|
||||
var resourceCosts = new HashSet<UpgradeResourceCost>();
|
||||
|
||||
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", "");
|
||||
if (!nameIdentifier.IsEmpty)
|
||||
@@ -383,6 +434,11 @@ namespace Barotrauma
|
||||
maxLevels.Add(new UpgradeMaxLevelMod(subElement));
|
||||
break;
|
||||
}
|
||||
case "resourcecost":
|
||||
{
|
||||
resourceCosts.Add(new UpgradeResourceCost(subElement));
|
||||
break;
|
||||
}
|
||||
#if CLIENT
|
||||
case "decorativesprite":
|
||||
{
|
||||
@@ -414,6 +470,7 @@ namespace Barotrauma
|
||||
|
||||
this.targetProperties = targetProperties;
|
||||
MaxLevelsMods = maxLevels.ToImmutableArray();
|
||||
ResourceCosts = resourceCosts.ToImmutableHashSet();
|
||||
|
||||
upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty<Identifier>())?
|
||||
.ToImmutableHashSet() ?? ImmutableHashSet<Identifier>.Empty;
|
||||
@@ -456,6 +513,58 @@ namespace Barotrauma
|
||||
return GetMaxLevel(info) > 0;
|
||||
}
|
||||
|
||||
public bool HasResourcesToUpgrade(Character? character, int currentLevel)
|
||||
{
|
||||
if (character is null) { return false; }
|
||||
if (!ResourceCosts.Any()) { return true; }
|
||||
|
||||
List<Item> allItems = character.Inventory.FindAllItems(recursive: true);
|
||||
return ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)).All(cost => cost.Amount <= allItems.Count(cost.MatchesItem));
|
||||
}
|
||||
|
||||
public bool TryTakeResources(Character character, int currentLevel)
|
||||
{
|
||||
IEnumerable<UpgradeResourceCost> costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel));
|
||||
|
||||
if (!costs.Any()) { return true; }
|
||||
|
||||
List<Item> allItems = character.Inventory.FindAllItems(recursive: true);
|
||||
HashSet<Item> itemsToRemove = new HashSet<Item>();
|
||||
|
||||
foreach (UpgradeResourceCost cost in costs)
|
||||
{
|
||||
int amountNeeded = cost.Amount;
|
||||
foreach (Item item in allItems.Where(cost.MatchesItem))
|
||||
{
|
||||
itemsToRemove.Add(item);
|
||||
amountNeeded--;
|
||||
if (amountNeeded <= 0) { break; }
|
||||
}
|
||||
|
||||
if (amountNeeded > 0) { return false; }
|
||||
}
|
||||
|
||||
foreach (Item item in itemsToRemove)
|
||||
{
|
||||
Entity.Spawner.AddItemToRemoveQueue(item);
|
||||
}
|
||||
|
||||
if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public ImmutableArray<ApplicableResourceCollection> GetApplicableResources(int level)
|
||||
{
|
||||
var applicableCosts = ResourceCosts.Where(cost => cost.AppliesForLevel(level)).ToImmutableHashSet();
|
||||
|
||||
var costs = applicableCosts.Any()
|
||||
? applicableCosts.Select(ApplicableResourceCollection.CreateFor).ToImmutableArray()
|
||||
: ImmutableArray<ApplicableResourceCollection>.Empty;
|
||||
|
||||
return costs;
|
||||
}
|
||||
|
||||
public bool IsDisallowed(MapEntity item)
|
||||
{
|
||||
return item.DisallowedUpgradeSet.Contains(Identifier)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user