Build 0.21.6.0
This commit is contained in:
@@ -351,21 +351,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
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 void ReequipUnequipped()
|
||||
{
|
||||
foreach (var item in unequippedItems)
|
||||
|
||||
@@ -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,7 +361,7 @@ namespace Barotrauma
|
||||
{
|
||||
targetingTag = "owner";
|
||||
}
|
||||
else if (targetCharacter.AIController is HumanAIController && !IsOnFriendlyTeam(Character, targetCharacter))
|
||||
else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter)))
|
||||
{
|
||||
targetingTag = "hostile";
|
||||
}
|
||||
@@ -681,19 +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 (attacker?.AiTarget != null && !Character.IsSameSpeciesOrGroup(attacker) && !targetCharacter.IsSameSpeciesOrGroup(attacker))
|
||||
Character attacker = targetCharacter.LastAttackers.LastOrDefault(ShouldRetaliate)?.Character;
|
||||
if (attacker?.AiTarget != null)
|
||||
{
|
||||
// Can't retaliate on characters of same species or group because that would make us hostile to all friendly characters in the same group.
|
||||
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
|
||||
SelectTarget(attacker.AiTarget);
|
||||
State = AIState.Attack;
|
||||
@@ -1502,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;
|
||||
}
|
||||
@@ -2316,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
|
||||
@@ -1567,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>();
|
||||
|
||||
@@ -2045,7 +2045,7 @@ namespace Barotrauma
|
||||
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
|
||||
{
|
||||
bool sameTeam = me.TeamID == other.TeamID;
|
||||
bool teamGood = sameTeam || !onlySameTeam && IsOnFriendlyTeam(me, other);
|
||||
bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other);
|
||||
if (!teamGood) { return false; }
|
||||
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
|
||||
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
|
||||
+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);
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
|
||||
+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 && !AIController.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,
|
||||
|
||||
+3
-15
@@ -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)
|
||||
{
|
||||
@@ -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);
|
||||
|
||||
|
||||
+1
-1
@@ -454,7 +454,7 @@ namespace Barotrauma
|
||||
aiming = false;
|
||||
wasAimingMelee = aimingMelee;
|
||||
aimingMelee = false;
|
||||
IsHanging = false;
|
||||
IsHanging = IsHanging && character.IsRagdolled;
|
||||
}
|
||||
|
||||
void UpdateStanding()
|
||||
|
||||
@@ -489,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}");
|
||||
}
|
||||
@@ -752,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,6 +822,7 @@ namespace Barotrauma
|
||||
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
|
||||
{
|
||||
@@ -1040,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;
|
||||
@@ -2871,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)
|
||||
@@ -3791,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)
|
||||
@@ -3803,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
|
||||
@@ -4122,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);
|
||||
@@ -4143,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>
|
||||
@@ -5240,7 +5324,24 @@ namespace Barotrauma
|
||||
|
||||
public bool IsFriendly(Character other) => IsFriendly(this, other);
|
||||
|
||||
public static bool IsFriendly(Character me, Character other) => AIController.IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other);
|
||||
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);
|
||||
|
||||
|
||||
+25
-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())
|
||||
{
|
||||
|
||||
@@ -708,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)
|
||||
{
|
||||
|
||||
@@ -490,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
|
||||
{
|
||||
@@ -640,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;
|
||||
@@ -772,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)
|
||||
{
|
||||
@@ -791,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; }
|
||||
|
||||
|
||||
+3
-6
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ partial class UIHighlightAction : EventAction
|
||||
TurbineOutputSlider,
|
||||
DeconstructButton,
|
||||
RechargeSpeedSlider,
|
||||
CPRButton
|
||||
CPRButton,
|
||||
CloseButton,
|
||||
MessageBoxCloseButton
|
||||
}
|
||||
|
||||
[Serialize(ElementId.None, IsPropertySaveable.Yes)]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -692,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()
|
||||
{
|
||||
@@ -713,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;
|
||||
|
||||
@@ -289,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,10 @@ 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 / Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f &&
|
||||
|
||||
//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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -223,7 +223,7 @@ 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.LockFlipping();
|
||||
@@ -472,8 +472,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null)
|
||||
{
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, targetEntity));
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, targetEntity));
|
||||
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}");
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace Barotrauma.Items.Components
|
||||
get { return allowedSlots; }
|
||||
}
|
||||
|
||||
public bool PickingDone => pickTimer >= PickingTime;
|
||||
|
||||
public Character Picker
|
||||
{
|
||||
get
|
||||
|
||||
@@ -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, useTarget: 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));
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -993,8 +1000,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
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
|
||||
@@ -1003,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1012,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;
|
||||
}
|
||||
@@ -1028,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),
|
||||
|
||||
@@ -302,33 +302,16 @@ namespace Barotrauma.Items.Components
|
||||
var sourceBody = GetBodyToPull(source);
|
||||
if (sourceBody != null)
|
||||
{
|
||||
var targetBody = GetBodyToPull(target);
|
||||
if (targetBody != null && targetBody.UserData is not 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;
|
||||
@@ -341,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,13 @@ 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)
|
||||
@@ -241,6 +248,7 @@ namespace Barotrauma.Items.Components
|
||||
SetLightSourceState(IsActive);
|
||||
turret = item.GetComponent<Turret>();
|
||||
#if CLIENT
|
||||
Drawable = AlphaBlend && Light.LightSprite != null;
|
||||
if (Screen.Selected.IsEditor)
|
||||
{
|
||||
OnMapLoaded();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -585,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
|
||||
|
||||
@@ -897,7 +897,7 @@ namespace Barotrauma
|
||||
defaultRect = newRect;
|
||||
rect = newRect;
|
||||
|
||||
condition = MaxCondition = Prefab.Health;
|
||||
condition = MaxCondition = prevCondition = Prefab.Health;
|
||||
ConditionPercentage = 100.0f;
|
||||
|
||||
lastSentCondition = condition;
|
||||
@@ -999,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;
|
||||
}
|
||||
}
|
||||
@@ -1020,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)
|
||||
{
|
||||
@@ -1751,6 +1752,7 @@ namespace Barotrauma
|
||||
|
||||
RecalculateConditionValues();
|
||||
|
||||
bool wasPreviousConditionChanged = false;
|
||||
if (condition == 0.0f && prevCondition > 0.0f)
|
||||
{
|
||||
//Flag connections to be updated as device is broken
|
||||
@@ -1763,6 +1765,8 @@ 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 && prevCondition <= 0.0f)
|
||||
@@ -1793,9 +1797,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
LastConditionChange = condition - prevCondition;
|
||||
ConditionLastUpdated = Timing.TotalTime;
|
||||
prevCondition = condition;
|
||||
if (!wasPreviousConditionChanged)
|
||||
{
|
||||
SetPreviousCondition();
|
||||
}
|
||||
|
||||
void SetPreviousCondition()
|
||||
{
|
||||
LastConditionChange = condition - prevCondition;
|
||||
ConditionLastUpdated = Timing.TotalTime;
|
||||
prevCondition = condition;
|
||||
wasPreviousConditionChanged = true;
|
||||
}
|
||||
|
||||
static void flagChangedConnections(Dictionary<string, Connection> connections)
|
||||
{
|
||||
@@ -2696,7 +2709,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (condition == 0.0f) { return; }
|
||||
if (condition <= 0.0f) { return; }
|
||||
|
||||
bool remove = false;
|
||||
|
||||
@@ -2713,7 +2726,7 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
ic.PlaySound(ActionType.OnUse, character);
|
||||
#endif
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: targetLimb?.character, user: character);
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: character, user: character);
|
||||
|
||||
if (ic.DeleteOnUse) { remove = true; }
|
||||
}
|
||||
@@ -2727,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;
|
||||
|
||||
@@ -2763,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));
|
||||
@@ -2783,13 +2803,13 @@ namespace Barotrauma
|
||||
#endif
|
||||
ic.WasUsed = true;
|
||||
|
||||
ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user);
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: targetLimb?.character, 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; }
|
||||
|
||||
@@ -769,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);
|
||||
@@ -938,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);
|
||||
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -302,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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2073,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2111,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2170,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);
|
||||
}
|
||||
@@ -2183,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -546,7 +546,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (Item item in itemsToRemove)
|
||||
{
|
||||
item.Remove();
|
||||
Entity.Spawner.AddItemToRemoveQueue(item);
|
||||
}
|
||||
|
||||
if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); }
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
namespace Barotrauma.Utils;
|
||||
|
||||
public struct CoordinateSpace2D
|
||||
{
|
||||
public static readonly CoordinateSpace2D CanonicalSpace = new CoordinateSpace2D
|
||||
{
|
||||
Origin = Vector2.Zero,
|
||||
I = Vector2.UnitX,
|
||||
J = Vector2.UnitY
|
||||
};
|
||||
|
||||
public Vector2 Origin;
|
||||
public Vector2 I;
|
||||
public Vector2 J;
|
||||
|
||||
public Matrix LocalToCanonical
|
||||
=> new Matrix(
|
||||
m11: I.X, m12: I.Y, m13: 0f, m14: 0f,
|
||||
m21: J.X, m22: J.Y, m23: 0f, m24: 0f,
|
||||
m31: 0f, m32: 0f, m33: 1f, m34: 0f,
|
||||
m41: 0f, m42: 0f, m43: 0f, m44: 1f)
|
||||
* Matrix.CreateTranslation(Origin.X, Origin.Y, 0f);
|
||||
|
||||
public Matrix CanonicalToLocal => Matrix.Invert(LocalToCanonical);
|
||||
}
|
||||
@@ -19,6 +19,9 @@ namespace Barotrauma
|
||||
|
||||
static class MathUtils
|
||||
{
|
||||
public static Vector2 DiscardZ(this Vector3 vector)
|
||||
=> new Vector2(vector.X, vector.Y);
|
||||
|
||||
public static float Percentage(float portion, float total)
|
||||
{
|
||||
return portion / total * 100;
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Xml.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Barotrauma.IO;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -292,10 +293,11 @@ namespace Barotrauma
|
||||
}
|
||||
if (doc?.Root == null)
|
||||
{
|
||||
saveInfos.Add(new CampaignMode.SaveInfo()
|
||||
{
|
||||
FilePath = file
|
||||
});
|
||||
saveInfos.Add(new CampaignMode.SaveInfo(
|
||||
FilePath: file,
|
||||
SaveTime: Option.None,
|
||||
SubmarineName: "",
|
||||
EnabledContentPackageNames: ImmutableArray<string>.Empty));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -326,13 +328,11 @@ namespace Barotrauma
|
||||
enabledContentPackageNames.Add(packageName.Replace(@"\|", "|"));
|
||||
}
|
||||
|
||||
saveInfos.Add(new CampaignMode.SaveInfo()
|
||||
{
|
||||
FilePath = file,
|
||||
SubmarineName = doc?.Root?.GetAttributeStringUnrestricted("submarine", ""),
|
||||
SaveTime = doc.Root.GetAttributeInt("savetime", 0),
|
||||
EnabledContentPackageNames = enabledContentPackageNames.ToArray(),
|
||||
});
|
||||
saveInfos.Add(new CampaignMode.SaveInfo(
|
||||
FilePath: file,
|
||||
SaveTime: doc.Root.GetAttributeDateTime("savetime"),
|
||||
SubmarineName: doc?.Root?.GetAttributeStringUnrestricted("submarine", ""),
|
||||
EnabledContentPackageNames: enabledContentPackageNames.ToImmutableArray()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public readonly struct SerializableTimeZone
|
||||
{
|
||||
/// <summary>
|
||||
/// Diff from UTC
|
||||
/// </summary>
|
||||
public readonly TimeSpan Value;
|
||||
|
||||
private readonly int hours;
|
||||
private readonly int minutes;
|
||||
private readonly char sign;
|
||||
|
||||
public SerializableTimeZone(TimeSpan value)
|
||||
{
|
||||
Value = new TimeSpan(
|
||||
hours: value.Hours,
|
||||
minutes: value.Minutes,
|
||||
seconds: 0);
|
||||
|
||||
hours = Math.Abs(value.Hours);
|
||||
minutes = Math.Abs(value.Minutes);
|
||||
sign = Value.Ticks < 0 ? '-' : '+';
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> (hours, minutes) switch
|
||||
{
|
||||
(0, 0) => "UTC",
|
||||
(_, 0) => $"UTC{sign}{hours}",
|
||||
(_, < 10) => $"UTC{sign}{hours}:0{minutes}",
|
||||
_ => $"UTC{sign}{hours}:{minutes}"
|
||||
};
|
||||
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(Value.Ticks < 0, hours, minutes);
|
||||
|
||||
public static SerializableTimeZone FromDateTime(DateTime dateTime)
|
||||
{
|
||||
if (dateTime.Kind == DateTimeKind.Unspecified)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot determine timezone for {nameof(DateTime)} " +
|
||||
$"of unspecified kind");
|
||||
}
|
||||
var utcDateTime = dateTime.ToUniversalTime();
|
||||
return new SerializableTimeZone(dateTime - utcDateTime);
|
||||
}
|
||||
|
||||
public static SerializableTimeZone LocalTimeZone
|
||||
=> FromDateTime(DateTime.Now);
|
||||
|
||||
public static Option<SerializableTimeZone> Parse(string str)
|
||||
{
|
||||
if (!str.StartsWith("UTC", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Option<SerializableTimeZone>.None();
|
||||
}
|
||||
string timeZoneStr = str[3..];
|
||||
bool negative = timeZoneStr.StartsWith("-");
|
||||
bool valid = negative || timeZoneStr.StartsWith("+");
|
||||
|
||||
if (!valid) { return Option<SerializableTimeZone>.None(); }
|
||||
|
||||
timeZoneStr = str[4..];
|
||||
|
||||
TimeSpan makeTimeSpan(int hours, int minutes)
|
||||
=> new TimeSpan(
|
||||
ticks: (hours * TimeSpan.TicksPerHour + minutes * TimeSpan.TicksPerMinute)
|
||||
* (negative ? -1L : 1L));
|
||||
|
||||
if (timeZoneStr.IndexOf(':') is var hrMinSeparator && hrMinSeparator > 0)
|
||||
{
|
||||
if (int.TryParse(timeZoneStr[..hrMinSeparator], out int timeZoneHours)
|
||||
&& int.TryParse(timeZoneStr[(hrMinSeparator + 1)..], out int timeZoneMinutes))
|
||||
{
|
||||
return Option<SerializableTimeZone>.Some(
|
||||
new SerializableTimeZone(makeTimeSpan(timeZoneHours, timeZoneMinutes)));
|
||||
}
|
||||
}
|
||||
else if (int.TryParse(timeZoneStr, out int timeZoneHours))
|
||||
{
|
||||
return Option<SerializableTimeZone>.Some(
|
||||
new SerializableTimeZone(makeTimeSpan(timeZoneHours, 0)));
|
||||
}
|
||||
return Option<SerializableTimeZone>.None();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DateTime wrapper that tries to offer a reliable
|
||||
/// string representation that's also human-friendly
|
||||
/// </summary>
|
||||
public readonly struct SerializableDateTime : IComparable<SerializableDateTime>
|
||||
{
|
||||
public bool Equals(SerializableDateTime other)
|
||||
=> ToUtc().value.Equals(other.ToUtc().value);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is SerializableDateTime other && Equals(other);
|
||||
|
||||
private static DateTime UnixEpoch(DateTimeKind kind)
|
||||
=> new DateTime(1970, 1, 1, 0, 0, 0, kind);
|
||||
|
||||
private readonly DateTime value;
|
||||
public readonly SerializableTimeZone TimeZone;
|
||||
|
||||
public SerializableDateTime(DateTime value) : this(value, default)
|
||||
{
|
||||
if (value.Kind == DateTimeKind.Unspecified)
|
||||
{
|
||||
throw new Exception($"Timezone required when constructing {nameof(SerializableDateTime)} " +
|
||||
$"from {nameof(DateTime)} of unspecified kind");
|
||||
}
|
||||
TimeZone = SerializableTimeZone.FromDateTime(value);
|
||||
}
|
||||
|
||||
public SerializableDateTime(DateTime value, SerializableTimeZone timeZone)
|
||||
{
|
||||
this.value = new DateTime(
|
||||
value.Year, value.Month, value.Day,
|
||||
value.Hour, value.Minute, value.Second,
|
||||
DateTimeKind.Unspecified);
|
||||
TimeZone = timeZone;
|
||||
}
|
||||
|
||||
public static SerializableDateTime LocalNow
|
||||
=> new SerializableDateTime(DateTime.Now);
|
||||
|
||||
public static SerializableDateTime UtcNow
|
||||
=> new SerializableDateTime(DateTime.UtcNow);
|
||||
|
||||
public SerializableDateTime ToUtc()
|
||||
=> new SerializableDateTime(
|
||||
DateTime.SpecifyKind(value - TimeZone.Value, DateTimeKind.Utc));
|
||||
|
||||
public SerializableDateTime ToLocal()
|
||||
=> new SerializableDateTime(
|
||||
DateTime.SpecifyKind(
|
||||
value - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value,
|
||||
DateTimeKind.Local));
|
||||
|
||||
public long Ticks => value.Ticks;
|
||||
|
||||
public DateTime ToUtcValue() => ToUtc().value;
|
||||
public DateTime ToLocalValue() => ToLocal().value;
|
||||
|
||||
public static SerializableDateTime FromLocalUnixTime(long unixTime)
|
||||
=> new SerializableDateTime(UnixEpoch(DateTimeKind.Local) + TimeSpan.FromSeconds(unixTime));
|
||||
|
||||
public static SerializableDateTime FromUtcUnixTime(long unixTime)
|
||||
=> new SerializableDateTime(UnixEpoch(DateTimeKind.Utc) + TimeSpan.FromSeconds(unixTime));
|
||||
|
||||
public long ToUnixTime()
|
||||
=> (value - UnixEpoch(value.Kind)).Ticks / TimeSpan.TicksPerSecond;
|
||||
|
||||
private static string MakeString(params (long Value, string Suffix)[] parts)
|
||||
=> string.Join(' ',
|
||||
parts.Select(p => $"{p.Value.ToString().PadLeft(2, '0')}{p.Suffix}"));
|
||||
|
||||
public override string ToString()
|
||||
=> MakeString(
|
||||
// Let's go out of our way to tag
|
||||
// the year, month and day so nobody
|
||||
// gets confused about the meaning of
|
||||
// each number
|
||||
(value.Year, "Y"),
|
||||
(value.Month, "M"),
|
||||
(value.Day, "D"),
|
||||
|
||||
(value.Hour, "HR"),
|
||||
(value.Minute, "MIN"),
|
||||
(value.Second, "SEC"))
|
||||
+ $" {TimeZone}";
|
||||
|
||||
public string ToLocalUserString()
|
||||
=> ToLocalValue().ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(
|
||||
value.Year, value.Month, value.Day,
|
||||
value.Hour, value.Minute, value.Second,
|
||||
TimeZone.GetHashCode());
|
||||
|
||||
public static Option<SerializableDateTime> Parse(string str)
|
||||
{
|
||||
if (long.TryParse(str, out long unixTime)
|
||||
&& unixTime > 0
|
||||
&& unixTime < (DateTime.MaxValue - UnixEpoch(DateTimeKind.Utc)).TotalSeconds)
|
||||
{
|
||||
return Option<SerializableDateTime>.Some(FromUtcUnixTime(unixTime));
|
||||
}
|
||||
|
||||
string[] split = str.Split(' ');
|
||||
|
||||
int year = 0; int month = 0; int day = 0;
|
||||
int hour = 0; int minute = 0; int second = 0;
|
||||
SerializableTimeZone timeZone = default;
|
||||
foreach (var part in split)
|
||||
{
|
||||
if (SerializableTimeZone.Parse(part).TryUnwrap(out var parsedTimeZone))
|
||||
{
|
||||
timeZone = parsedTimeZone;
|
||||
continue;
|
||||
}
|
||||
|
||||
Identifier suffix = string.Join("", part.Where(char.IsLetter)).ToIdentifier();
|
||||
if (!part.EndsWith(suffix.Value)) { continue; }
|
||||
if (!int.TryParse(
|
||||
part[..^suffix.Value.Length],
|
||||
NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture,
|
||||
out int value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (suffix == "Y") { year = value; }
|
||||
else if (suffix == "M") { month = value; }
|
||||
else if (suffix == "D") { day = value; }
|
||||
else if (suffix == "HR") { hour = value; }
|
||||
else if (suffix == "MIN") { minute = value; }
|
||||
else if (suffix == "SEC") { second = value; }
|
||||
}
|
||||
|
||||
if (year > 0 && month > 0 && day > 0)
|
||||
{
|
||||
return Option<SerializableDateTime>.Some(
|
||||
new SerializableDateTime(
|
||||
new DateTime(year, month, day, hour, minute, second),
|
||||
timeZone));
|
||||
}
|
||||
|
||||
return Option<SerializableDateTime>.None();
|
||||
}
|
||||
|
||||
public int CompareTo(SerializableDateTime other)
|
||||
=> ToUtc().value.CompareTo(other.ToUtc().value);
|
||||
|
||||
public static bool operator <(in SerializableDateTime a, in SerializableDateTime b)
|
||||
=> a.CompareTo(b) < 0;
|
||||
|
||||
public static bool operator >(in SerializableDateTime a, in SerializableDateTime b)
|
||||
=> a.CompareTo(b) > 0;
|
||||
|
||||
public static bool operator ==(in SerializableDateTime a, in SerializableDateTime b)
|
||||
=> a.CompareTo(b) == 0;
|
||||
|
||||
public static bool operator !=(in SerializableDateTime a, in SerializableDateTime b)
|
||||
=> !(a == b);
|
||||
|
||||
public static SerializableDateTime operator +(in SerializableDateTime dt, in TimeSpan ts)
|
||||
=> new SerializableDateTime(dt.value + ts, dt.TimeZone);
|
||||
|
||||
public static SerializableDateTime operator -(in SerializableDateTime dt, in TimeSpan ts)
|
||||
=> new SerializableDateTime(dt.value - ts, dt.TimeZone);
|
||||
|
||||
public static TimeSpan operator -(in SerializableDateTime a, in SerializableDateTime b)
|
||||
=> a.ToUtc().value - b.ToUtc().value;
|
||||
}
|
||||
}
|
||||
@@ -29,49 +29,6 @@ namespace Barotrauma
|
||||
|
||||
static partial class ToolBox
|
||||
{
|
||||
internal static class Epoch
|
||||
{
|
||||
private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current Unix Epoch (Coordinated Universal Time)
|
||||
/// </summary>
|
||||
public static int NowUTC
|
||||
{
|
||||
get
|
||||
{
|
||||
return (int)(DateTime.UtcNow.Subtract(epoch).TotalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current Unix Epoch (user's current time)
|
||||
/// </summary>
|
||||
public static int NowLocal
|
||||
{
|
||||
get
|
||||
{
|
||||
return (int)(DateTime.Now.Subtract(epoch).TotalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an epoch to a datetime
|
||||
/// </summary>
|
||||
public static DateTime ToDateTime(decimal unixTime)
|
||||
{
|
||||
return epoch.AddSeconds((long)unixTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a DateTime to a unix time
|
||||
/// </summary>
|
||||
public static uint FromDateTime(DateTime dt)
|
||||
{
|
||||
return (uint)(dt.Subtract(epoch).TotalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsProperFilenameCase(string filename)
|
||||
{
|
||||
//File case only matters on Linux where the filesystem is case-sensitive, so we don't need these errors in release builds.
|
||||
|
||||
Reference in New Issue
Block a user