Build 0.20.4.0

This commit is contained in:
Markus Isberg
2022-11-11 17:57:23 +02:00
parent edaf4b09fe
commit 54712b5dc9
201 changed files with 7618 additions and 2020 deletions
@@ -1836,8 +1836,7 @@ namespace Barotrauma
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f);
}
}
if (AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
{
bool advance = !canAttack && Character.InWater || distance > attackLimb.attack.Range * 0.9f;
bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
@@ -1881,19 +1880,11 @@ namespace Barotrauma
}
}
}
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable;
if (AttackLimb?.attack is Attack { Ranged: true} attack)
Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity;
IDamageable damageTarget = targetEntity as IDamageable;
if (AttackLimb?.attack is Attack { Ranged: true} attack && targetEntity != null)
{
Limb limb = GetLimbToRotate(attack);
if (limb != null)
{
Vector2 toTarget = damageTarget.WorldPosition - limb.WorldPosition;
float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
limb.body.SuppressSmoothRotationCalls = false;
float angle = MathUtils.VectorToAngle(toTarget);
limb.body.SmoothRotate(angle + offset, attack.AimRotationTorque);
limb.body.SuppressSmoothRotationCalls = true;
}
AimRangedAttack(attack, targetEntity);
}
if (canAttack)
{
@@ -1908,6 +1899,22 @@ namespace Barotrauma
}
}
public void AimRangedAttack(Attack attack, Entity targetEntity)
{
if (attack == null || attack.Ranged == false || targetEntity == null) { return; }
Character.SetInput(InputType.Aim, false, true);
Limb limb = GetLimbToRotate(attack);
if (limb != null)
{
Vector2 toTarget = targetEntity.WorldPosition - limb.WorldPosition;
float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
limb.body.SuppressSmoothRotationCalls = false;
float angle = MathUtils.VectorToAngle(toTarget);
limb.body.SmoothRotate(angle + offset, attack.AimRotationTorque);
limb.body.SuppressSmoothRotationCalls = true;
}
}
private bool IsValidAttack(Limb attackingLimb, IEnumerable<AttackContext> currentContexts, Entity target)
{
if (attackingLimb == null) { return false; }
@@ -2190,11 +2197,11 @@ namespace Barotrauma
if (!ActiveAttack.IsRunning)
{
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
AttackLimb,
damageTarget,
targetLimb,
SimPosition));
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
AttackLimb,
damageTarget,
targetLimb,
SimPosition));
#else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif
@@ -3599,7 +3606,10 @@ namespace Barotrauma
{
// We only want to check the visibility when the target is in ruins/wreck/similiar place where sneaking should be possible.
// When the monsters attack the player sub, they wall hack so that they can be more aggressive.
checkVisibility = target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None;
// Pets should always check the visibility, unless the pet and the target are both outside the submarine -> shouldn't target when they can't perceive (= no wall hack)
checkVisibility =
Character.IsPet && (Character.Submarine == null) != (target.Entity.Submarine == null) ||
target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None;
}
if (dist > 0)
{
@@ -395,6 +395,10 @@ namespace Barotrauma
}
objectiveManager.UpdateObjectives(deltaTime);
if (reportProblemsTimer > 0)
{
reportProblemsTimer -= deltaTime;
}
if (reactTimer > 0.0f)
{
reactTimer -= deltaTime;
@@ -407,7 +411,6 @@ namespace Barotrauma
else
{
Character.UpdateTeam();
if (Character.CurrentHull != null)
{
if (Character.IsOnPlayerTeam)
@@ -425,19 +428,15 @@ namespace Barotrauma
}
}
}
if (Character.SpeechImpediment < 100.0f)
if (reportProblemsTimer <= 0.0f)
{
reportProblemsTimer -= deltaTime;
if (reportProblemsTimer <= 0.0f)
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
reportProblemsTimer = reportProblemsInterval;
ReportProblems();
}
UpdateSpeaking();
reportProblemsTimer = reportProblemsInterval;
}
UpdateSpeaking();
UnequipUnnecessaryItems();
reactTimer = GetReactionTime();
}
@@ -912,7 +911,7 @@ namespace Barotrauma
{
Order newOrder = null;
Hull targetHull = null;
bool speak = true;
bool speak = Character.SpeechImpediment < 100;
if (Character.CurrentHull != null)
{
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
@@ -1063,17 +1062,15 @@ namespace Barotrauma
private void UpdateSpeaking()
{
if (!Character.IsOnPlayerTeam) { return; }
if (Character.SpeechImpediment >= 100) { return; }
if (Character.Oxygen < 20.0f)
{
Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f);
}
if (Character.Bleeding > 2.0f)
{
Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f);
}
if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null)
{
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f);
@@ -1860,6 +1857,7 @@ namespace Barotrauma
bool targetAdded = false;
DoForEachCrewMember(caller, humanAI =>
{
if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
var objective = humanAI.ObjectiveManager.GetObjective<T1>();
if (objective != null)
{
@@ -502,10 +502,12 @@ namespace Barotrauma
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions())
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;
}
}
@@ -1836,8 +1836,6 @@ namespace Barotrauma
{
heldItem.FlipX(relativeToSub: false);
}
// TODO: was this added by a mistake?
//heldItem.FlipX(relativeToSub: false);
}
foreach (Limb limb in Limbs)
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -530,7 +531,7 @@ namespace Barotrauma
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
effect.AddNearbyTargets(worldPosition, targets);
effect.Apply(effectType, deltaTime, targetEntity, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
@@ -569,7 +570,15 @@ namespace Barotrauma
DamageParticles(deltaTime, worldPosition);
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration: Penetration);
float penetration = Penetration;
float? penetrationValue = SourceItem?.GetComponent<RangedWeapon>()?.Penetration;
if (penetrationValue.HasValue)
{
penetration += penetrationValue.Value;
}
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration);
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
foreach (StatusEffect effect in statusEffects)
@@ -599,7 +608,7 @@ namespace Barotrauma
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
effect.AddNearbyTargets(worldPosition, targets);
effect.Apply(effectType, deltaTime, targetLimb.character, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
@@ -1601,6 +1601,15 @@ namespace Barotrauma
if (Info?.Job == null) { return 0.0f; }
float skillLevel = Info.Job.GetSkillLevel(skillIdentifier);
if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType))
{
float skillOverride = GetStatValue(statType);
if (skillOverride > skillLevel)
{
skillLevel = skillOverride;
}
}
// apply multipliers first so that multipliers only affect base skill value
foreach (Affliction affliction in CharacterHealth.GetAllAfflictions())
{
@@ -1631,15 +1640,6 @@ namespace Barotrauma
skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType))
{
float skillOverride = GetStatValue(statType);
if (skillOverride > skillLevel)
{
skillLevel = skillOverride;
}
}
return skillLevel;
}
@@ -1972,6 +1972,15 @@ namespace Barotrauma
}
}
#endif
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this && IsKeyDown(InputType.Aim))
{
if (currentAttackTarget.AttackLimb?.attack is Attack { Ranged: true } attack && AIController is EnemyAIController enemyAi)
{
enemyAi.AimRangedAttack(attack, currentAttackTarget.DamageTarget as Entity);
}
}
if (attackCoolDown > 0.0f)
{
attackCoolDown -= deltaTime;
@@ -1982,7 +1991,7 @@ namespace Barotrauma
{
if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false)
{
currentAttackTarget = default(AttackTargetData);
currentAttackTarget = default;
}
currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _);
}
@@ -2077,19 +2086,22 @@ namespace Barotrauma
}
}
bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected;
if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem))
if (Inventory != null)
{
foreach (Item item in HeldItems)
bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected;
if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem))
{
tryUseItem(item, deltaTime);
}
foreach (Item item in Inventory.AllItems)
{
if (item.GetComponent<Wearable>() is { AllowUseWhenWorn: true } && HasEquippedItem(item))
foreach (Item item in HeldItems)
{
tryUseItem(item, deltaTime);
}
foreach (Item item in Inventory.AllItems)
{
if (item.GetComponent<Wearable>() is { AllowUseWhenWorn: true } && HasEquippedItem(item))
{
tryUseItem(item, deltaTime);
}
}
}
}
@@ -2170,6 +2182,8 @@ namespace Barotrauma
private AttackTargetData currentAttackTarget;
public void SetAttackTarget(Limb attackLimb, IDamageable damageTarget, Vector2 attackPos)
{
DebugConsole.NewMessage($"SetAttackTarget {this.ToString()}: " + (damageTarget?.ToString() ?? null));
currentAttackTarget = new AttackTargetData()
{
AttackLimb = attackLimb,
@@ -3824,7 +3838,7 @@ namespace Barotrauma
return attackResult;
}
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, Character attacker = null)
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, bool ignoreSeveranceProbabilityModifier = false, Character attacker = null)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
#if DEBUG
@@ -3852,7 +3866,7 @@ namespace Barotrauma
var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB;
if (referenceLimb != targetLimb) { continue; }
float probability = severLimbsProbability;
if (!IsDead && probability < 1)
if (!IsDead && !ignoreSeveranceProbabilityModifier)
{
probability *= joint.Params.SeveranceProbabilityModifier;
}
@@ -4111,7 +4125,7 @@ namespace Barotrauma
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets));
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, this, targets);
}
else if (statusEffect.targetLimbs != null)
@@ -4745,6 +4759,8 @@ namespace Barotrauma
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
public bool HasJob(Identifier identifier) => Info?.Job?.Prefab.Identifier == identifier;
public bool IsProtectedFromPressure()
{
return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
@@ -4975,7 +4991,7 @@ namespace Barotrauma
/// </summary>
private readonly Dictionary<StatTypes, float> wearableStatValues = new Dictionary<StatTypes, float>();
public float GetStatValue(StatTypes statType)
public float GetStatValue(StatTypes statType, bool includeSaved = true)
{
if (!IsHuman) { return 0f; }
@@ -4988,7 +5004,7 @@ namespace Barotrauma
{
statValue += CharacterHealth.GetStatValue(statType);
}
if (Info != null)
if (Info != null && includeSaved)
{
// could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change
statValue += Info.GetSavedStatValue(statType);
@@ -1233,10 +1233,6 @@ namespace Barotrauma
int prevAmount = ExperiencePoints;
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
if (isMissionExperience)
{
Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier);
}
experienceGainMultiplier.Value += Character?.GetStatValue(StatTypes.ExperienceGainMultiplier) ?? 0;
amount = (int)(amount * experienceGainMultiplier.Value);
@@ -1808,7 +1804,7 @@ namespace Barotrauma
{
if (SavedStatValues.TryGetValue(statType, out var statValues))
{
return statValues.Where(s => s.StatIdentifier == statIdentifier).Sum(v => v.StatValue);
return statValues.Where(value => ToolBox.StatIdentifierMatches(value.StatIdentifier, statIdentifier)).Sum(static v => v.StatValue);
}
else
{
@@ -429,7 +429,7 @@ namespace Barotrauma
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets));
statusEffect.AddNearbyTargets(characterHealth.Character.WorldPosition, targets);
statusEffect.Apply(type, deltaTime, characterHealth.Character, targets);
}
}
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Collections.Immutable;
namespace Barotrauma
{
@@ -214,7 +215,6 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.No)]
public Identifier DialogFlag { get; private set; }
[Serialize("", IsPropertySaveable.No)]
public Identifier Tag { get; private set; }
@@ -276,6 +276,47 @@ namespace Barotrauma
}
}
public class Description
{
public enum TargetType
{
Any,
Self,
OtherCharacter
}
public readonly LocalizedString Text;
public readonly Identifier TextTag;
public readonly float MinStrength, MaxStrength;
public readonly TargetType Target;
public Description(ContentXElement element, AfflictionPrefab affliction)
{
TextTag = element.GetAttributeIdentifier("textidentifier", Identifier.Empty);
if (!TextTag.IsEmpty)
{
Text = TextManager.Get(TextTag);
}
string text = element.GetAttributeString("text", string.Empty);
if (!text.IsNullOrEmpty())
{
Text = Text?.Fallback(text) ?? text;
}
else if (TextTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions.");
}
MinStrength = element.GetAttributeFloat(nameof(MinStrength), 0.0f);
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
if (MinStrength >= MaxStrength)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min.");
}
Target = element.GetAttributeEnum(nameof(Target), TargetType.Any);
}
}
public class PeriodicEffect
{
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
@@ -313,7 +354,6 @@ namespace Barotrauma
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
private bool disposed = false;
public override void Dispose() { }
public static IEnumerable<AfflictionPrefab> List => Prefabs;
@@ -330,15 +370,21 @@ namespace Barotrauma
//(e.g. mental health problems on head, lack of oxygen on torso...)
public readonly LimbType IndicatorLimb;
public readonly LocalizedString Name, Description;
public readonly LocalizedString Name;
public readonly Identifier TranslationIdentifier;
public readonly bool IsBuff;
public readonly bool AffectMachines;
public readonly bool HealableInMedicalClinic;
public readonly float HealCostMultiplier;
public readonly int BaseHealCost;
public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription;
private readonly LocalizedString defaultDescription;
public readonly ImmutableList<Description> Descriptions;
public readonly bool HideIconAfterDelay;
//how high the strength has to be for the affliction to take affect
public readonly float ActivationThreshold = 0.0f;
//how high the strength has to be for the affliction icon to be shown in the UI
@@ -355,6 +401,11 @@ namespace Barotrauma
//how strong the affliction needs to be before bots attempt to treat it
public readonly float TreatmentThreshold = 5.0f;
/// <summary>
/// Bots will not try to treat the affliction if the character has any of these afflictions
/// </summary>
public ImmutableHashSet<Identifier> IgnoreTreatmentIfAfflictedBy;
/// <summary>
/// The affliction is automatically removed after this time. 0 = unlimited
/// </summary>
@@ -413,13 +464,14 @@ namespace Barotrauma
{
Name = Name.Fallback(fallbackName);
}
Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}");
defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}");
string fallbackDescription = element.GetAttributeString("description", "");
if (!string.IsNullOrEmpty(fallbackDescription))
{
Description = Description.Fallback(fallbackDescription);
defaultDescription = defaultDescription.Fallback(fallbackDescription);
}
IsBuff = element.GetAttributeBool("isbuff", false);
IsBuff = element.GetAttributeBool(nameof(IsBuff), false);
AffectMachines = element.GetAttributeBool(nameof(AffectMachines), true);
HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic",
!IsBuff &&
@@ -428,6 +480,8 @@ namespace Barotrauma
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f);
BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0);
IgnoreTreatmentIfAfflictedBy = element.GetAttributeIdentifierArray(nameof(IgnoreTreatmentIfAfflictedBy), Array.Empty<Identifier>()).ToImmutableHashSet();
Duration = element.GetAttributeFloat(nameof(Duration), 0.0f);
if (element.GetAttribute("nameidentifier") != null)
@@ -445,30 +499,33 @@ namespace Barotrauma
}
}
ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f);
ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f));
ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold);
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f);
HideIconAfterDelay = element.GetAttributeBool(nameof(HideIconAfterDelay), false);
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold",
ActivationThreshold = element.GetAttributeFloat(nameof(ActivationThreshold), 0.0f);
ShowIconThreshold = element.GetAttributeFloat(nameof(ShowIconThreshold), Math.Max(ActivationThreshold, 0.05f));
ShowIconToOthersThreshold = element.GetAttributeFloat(nameof(ShowIconToOthersThreshold), ShowIconThreshold);
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
GrainBurst = element.GetAttributeFloat(nameof(GrainBurst), 0.0f);
ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold),
Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold));
TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f));
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 5.0f));
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f);
DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f);
KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f);
KarmaChangeOnApplied = element.GetAttributeFloat(nameof(KarmaChangeOnApplied), 0.0f);
CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", ""));
SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", ""));
IconColors = element.GetAttributeColorArray("iconcolors", null);
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false);
AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", "");
IconColors = element.GetAttributeColorArray(nameof(IconColors), null);
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false);
AchievementOnRemoved = element.GetAttributeIdentifier(nameof(AchievementOnRemoved), "");
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
List<Description> descriptions = new List<Description>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -485,15 +542,38 @@ namespace Barotrauma
case "effect":
case "periodiceffect":
break;
case "description":
descriptions.Add(new Description(subElement, this));
break;
default:
DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})");
break;
}
}
Descriptions = descriptions.ToImmutableList();
constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) });
}
public LocalizedString GetDescription(float strength, Description.TargetType targetType)
{
foreach (var description in Descriptions)
{
if (strength < description.MinStrength || strength > description.MaxStrength) { continue; }
switch (targetType)
{
case Description.TargetType.Self:
if (description.Target == Description.TargetType.OtherCharacter) { continue; }
break;
case Description.TargetType.OtherCharacter:
if (description.Target == Description.TargetType.Self) { continue; }
break;
}
return description.Text;
}
return defaultDescription;
}
public static void LoadAllEffects()
{
Prefabs.ForEach(p => p.LoadEffects());
@@ -104,7 +104,7 @@ namespace Barotrauma
public bool DoesBleed
{
get => Character.Params.Health.DoesBleed;
get => Character.Params.Health.DoesBleed && !Character.Params.IsMachine;
private set => Character.Params.Health.DoesBleed = value;
}
@@ -550,7 +550,7 @@ namespace Barotrauma
amount -= reduceAmount;
if (treatmentAction != null)
{
if (treatmentAction.Value == ActionType.OnUse)
if (treatmentAction.Value == ActionType.OnUse || treatmentAction.Value == ActionType.OnSuccess)
{
matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
}
@@ -690,6 +690,7 @@ namespace Barotrauma
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true)
{
if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; }
@@ -1076,6 +1077,7 @@ namespace Barotrauma
}
if (strength <= affliction.Prefab.TreatmentThreshold) { continue; }
if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
if (ignoreHiddenAfflictions)
{
@@ -79,27 +79,35 @@ namespace Barotrauma
public ref readonly ImmutableArray<Identifier> ParsedAfflictionTypes => ref parsedAfflictionTypes;
public DamageModifier(XElement element, string parentDebugName)
public DamageModifier(XElement element, string parentDebugName, bool checkErrors = true)
{
Deserialize(element);
if (element.Attribute("afflictionnames") != null)
{
DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.");
}
foreach (var afflictionType in parsedAfflictionTypes)
if (checkErrors)
{
if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType))
foreach (var afflictionType in parsedAfflictionTypes)
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?");
}
}
foreach (var afflictionIdentifier in parsedAfflictionIdentifiers)
{
if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier))
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?");
if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType))
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?");
}
}
foreach (var afflictionIdentifier in parsedAfflictionIdentifiers)
{
if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier))
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?");
}
}
if (!parsedAfflictionTypes.Any() && !parsedAfflictionIdentifiers.Any())
{
createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Neither affliction types of identifiers defined.");
}
}
static void createWarningOrError(string msg)
{
#if DEBUG
@@ -1196,7 +1196,7 @@ namespace Barotrauma
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets));
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, character, targets);
}
else
@@ -50,6 +50,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable]
public bool NeedsWater { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Is this creature an artificial creature, like robot or machine that shouldn't be affected by afflictions that affect only organic creatures? Overrides DoesBleed."), Editable]
public bool IsMachine { get; set; }
[Serialize(false, IsPropertySaveable.No), Editable]
public bool CanSpeak { get; set; }
@@ -27,7 +27,8 @@ namespace Barotrauma.Abilities
{
if (isPositiveReputation)
{
if (abilityLocation.Location.Reputation.Faction.Reputation.Value <= 0) { return false; }
if (abilityLocation.Location?.Reputation is not { } reputation) { return false; }
if (reputation.Value <= 0) { return false; }
}
if (locationIdentifiers.Any())
@@ -33,13 +33,20 @@ namespace Barotrauma.Abilities
{
if (abilityObject is IAbilityMission { Mission: { } mission })
{
if (isAffiliated && GameMain.GameSession?.Campaign?.Factions.MaxBy(static f => f.Reputation.Value) is { } highestFaction)
if (isAffiliated)
{
if (highestFaction.Reputation.Value < 0 || !mission.ReputationRewards.ContainsKey(highestFaction.Reputation.Identifier))
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
// FIXME there's probably a better way to check the faction affiliated with the mission later
foreach (Identifier factionIdentifier in mission.ReputationRewards.Keys)
{
return false;
if (factions.Where(faction => factionIdentifier == faction.Prefab.Identifier).Any(static faction => faction.GetPlayerAffiliationStatus() != FactionAffiliation.Affiliated))
{
return false;
}
}
}
return missionType.Contains(mission.Prefab.Type);
}
@@ -1,9 +1,5 @@
using System;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using System;
namespace Barotrauma.Abilities
{
@@ -24,7 +24,7 @@ internal sealed class AbilityConditionNearbyCharacterCount : AbilityConditionDat
foreach (Character otherCharacter in Character.CharacterList)
{
if (character.Submarine != otherCharacter.Submarine) { continue; }
if (!IsViableTarget(targetTypes, otherCharacter)) { return false; }
if (!IsViableTarget(targetTypes, otherCharacter)) { continue; }
if (Vector2.DistanceSquared(character.WorldPosition, otherCharacter.WorldPosition) < distance * distance)
{
@@ -50,7 +50,7 @@ namespace Barotrauma.Abilities
else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets));
statusEffect.AddNearbyTargets(targetCharacter.WorldPosition, targets);
if (!nearbyCharactersAppliesToSelf)
{
targets.RemoveAll(c => c == Character);
@@ -27,6 +27,8 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect()
{
if (Character?.Submarine is null) { return; }
foreach (Item item in Character.Submarine.GetItems(true))
{
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
@@ -0,0 +1,25 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityGiveTalentPointsToAllies : CharacterAbility
{
private readonly int amount;
public CharacterAbilityGiveTalentPointsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
}
public override void InitializeAbility(bool addingFirstTime)
{
if (!addingFirstTime) { return; }
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (character.Info is null) { return; }
character.Info.AdditionalTalentPoints += amount;
}
}
}
}
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -13,6 +13,11 @@ namespace Barotrauma.Abilities
value = abilityElement.GetAttributeInt("value", 0);
}
public override void InitializeAbility(bool addingFirstTime)
{
ApplyEffect();
}
protected override void ApplyEffect()
{
if (identifier == Identifier.Empty) { return; }
@@ -12,6 +12,8 @@ namespace Barotrauma.Abilities
public override void InitializeAbility(bool addingFirstTime)
{
if (!addingFirstTime) { return; }
JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet());
if (apprentice is null)
{
@@ -27,39 +27,40 @@ namespace Barotrauma.Abilities
TimeSinceLastUpdate += deltaTime;
if (TimeSinceLastUpdate < interval) { return; }
bool shouldApplyDelayedEffect;
bool conditionsDidntMatch;
bool conditionsMatched;
if (AllConditionsMatched())
{
effectDelayTimer += TimeSinceLastUpdate;
shouldApplyDelayedEffect = effectDelayTimer >= effectDelay;
conditionsDidntMatch = false;
bool shouldApplyDelayedEffect = effectDelayTimer >= effectDelay;
conditionsMatched = shouldApplyDelayedEffect;
}
else
{
effectDelayTimer = 0f;
shouldApplyDelayedEffect = false;
conditionsDidntMatch = true;
conditionsMatched = false;
}
bool hasFallbacks = fallbackAbilities.Count > 0;
List<CharacterAbility> abilitiesToRun =
conditionsDidntMatch && hasFallbacks
!conditionsMatched && hasFallbacks
? fallbackAbilities
: characterAbilities;
if (hasFallbacks)
{
conditionsMatched = true;
}
foreach (var characterAbility in abilitiesToRun)
{
if (!characterAbility.IsViable()) { continue; }
characterAbility.UpdateCharacterAbility(
shouldApplyDelayedEffect || conditionsDidntMatch,
TimeSinceLastUpdate);
characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate);
}
if (shouldApplyDelayedEffect || (conditionsDidntMatch && hasFallbacks))
if (conditionsMatched)
{
timesTriggered++;
}
@@ -7,7 +7,7 @@ namespace Barotrauma
{
internal sealed class TalentTree : Prefab
{
public enum TalentTreeStageState
public enum TalentStages
{
Invalid,
Locked,
@@ -72,30 +72,30 @@ namespace Barotrauma
// i hate this function - markus
// me too - joonas
public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, IReadOnlyCollection<Identifier> selectedTalents)
public static TalentStages GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, IReadOnlyCollection<Identifier> selectedTalents)
{
if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; }
if (character?.Info?.Job.Prefab is null) { return TalentStages.Invalid; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentStages.Invalid; }
TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier);
if (subTree is null) { return TalentTreeStageState.Invalid; }
if (subTree is null) { return TalentStages.Invalid; }
if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents))
{
return TalentTreeStageState.Locked;
return TalentStages.Locked;
}
TalentOption targetTalentOption = subTree.TalentOptionStages[index];
if (targetTalentOption.HasEnoughTalents(character.Info))
{
return TalentTreeStageState.Unlocked;
return TalentStages.Unlocked;
}
if (targetTalentOption.HasSelectedTalent(selectedTalents))
{
return TalentTreeStageState.Highlighted;
return TalentStages.Highlighted;
}
bool hasTalentInLastTier = true;
@@ -111,17 +111,17 @@ namespace Barotrauma
if (!hasTalentInLastTier)
{
return TalentTreeStageState.Locked;
return TalentStages.Locked;
}
bool hasPointsForNewTalent = character.Info.GetTotalTalentPoints() - selectedTalents.Count > 0;
if (hasPointsForNewTalent)
{
return isLastTalentPurchased ? TalentTreeStageState.Highlighted : TalentTreeStageState.Available;
return isLastTalentPurchased ? TalentStages.Highlighted : TalentStages.Available;
}
return TalentTreeStageState.Locked;
return TalentStages.Locked;
}
@@ -207,7 +207,7 @@ namespace Barotrauma
{
nameIdentifier = $"talenttree.{Identifier}";
}
DisplayName = TextManager.Get($"talenttree.{nameIdentifier}").Fallback(Identifier.Value);
DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value);
Type = subTreeElement.GetAttributeEnum("type", TalentTreeType.Specialization);
RequiredTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("requires", ImmutableHashSet<Identifier>.Empty);
BlockedTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("blocks", ImmutableHashSet<Identifier>.Empty);
@@ -234,7 +234,7 @@ namespace Barotrauma
/// When specified the talent option will show talent with this identifier
/// and clicking on it will expand the talent option to show the talents
/// </summary>
public readonly Option<Identifier> ShowcaseTalent;
public readonly Dictionary<Identifier, ImmutableHashSet<Identifier>> ShowCaseTalents = new Dictionary<Identifier, ImmutableHashSet<Identifier>>();
public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= MaxChosenTalents;
public bool HasEnoughTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents;
@@ -269,19 +269,30 @@ namespace Barotrauma
{
MaxChosenTalents = talentOptionsElement.GetAttributeInt("maxchosentalents", 1);
Identifier showcaseTalent = talentOptionsElement.GetAttributeIdentifier("showcasetalent", Identifier.Empty);
ShowcaseTalent = !showcaseTalent.IsEmpty
? Option<Identifier>.Some(showcaseTalent)
: Option<Identifier>.None();
HashSet<Identifier> identifiers = new HashSet<Identifier>();
var talentIdentifiers = new HashSet<Identifier>();
foreach (var talentOptionElement in talentOptionsElement.GetChildElements("talentoption"))
foreach (ContentXElement talentOptionElement in talentOptionsElement.Elements())
{
Identifier identifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
talentIdentifiers.Add(identifier);
Identifier elementName = talentOptionElement.Name.ToIdentifier();
if (elementName == "talentoption")
{
identifiers.Add(talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty));
}
else if (elementName == "showcasetalent")
{
Identifier showCaseIdentifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
HashSet<Identifier> showCaseTalentIdentifiers = new HashSet<Identifier>();
foreach (ContentXElement subElement in talentOptionElement.Elements())
{
Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
showCaseTalentIdentifiers.Add(identifier);
identifiers.Add(identifier);
}
ShowCaseTalents.Add(showCaseIdentifier, showCaseTalentIdentifiers.ToImmutableHashSet());
}
}
this.talentIdentifiers = talentIdentifiers.ToImmutableHashSet();
talentIdentifiers = identifiers.ToImmutableHashSet();
}
}
}
@@ -18,7 +18,7 @@ namespace Barotrauma
public const string LocalModsDir = "LocalMods";
public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine(
SaveUtil.SaveFolder,
SaveUtil.DefaultSaveFolder,
"WorkshopMods",
"Installed");
@@ -125,6 +125,7 @@ namespace Barotrauma
internal int IndexOf(char c) => Value.IndexOf(c);
internal Identifier this[Range range] => Value[range].ToIdentifier();
internal Char this[int i] => Value[i];
}
public static class IdentifierExtensions
@@ -2199,7 +2199,7 @@ namespace Barotrauma
//Dont do a thing, random is basically Human points anyways - its in the help description.
break;
default:
var matchingCharacter = FindMatchingCharacter(args.Skip(1).ToArray());
var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray());
if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; }
break;
}
@@ -154,6 +154,7 @@ namespace Barotrauma
BallastFloraDamageMultiplier,
HoldBreathMultiplier,
Apprenticeship,
Affiliation,
CPRBoost
}
@@ -25,6 +25,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes)]
public bool RequireEquipped { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
public bool Recursive { get; set; }
[Serialize(-1, IsPropertySaveable.Yes)]
public int ItemContainerIndex { get; set; }
@@ -97,7 +100,7 @@ namespace Barotrauma
{
if (inventory == null) { return false; }
int count = 0;
foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier)))
foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier), recursive: Recursive))
{
if (!ConditionalsMatch(item, character)) { continue; }
count++;
@@ -0,0 +1,58 @@
using System;
using System.Linq;
namespace Barotrauma;
class CheckMissionAction : BinaryOptionAction
{
public enum MissionType
{
Current,
Selected,
Available
}
[Serialize(MissionType.Current, IsPropertySaveable.Yes)]
public MissionType Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionTag { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
public int MissionCount { get; set; }
public CheckMissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
MissionCount = Math.Max(MissionCount, 0);
}
protected override bool? DetermineSuccess()
{
var missions = Type switch
{
MissionType.Current => GameMain.GameSession?.Missions,
MissionType.Selected => GameMain.GameSession?.Campaign?.Missions,
MissionType.Available => GameMain.GameSession?.Map?.CurrentLocation?.AvailableMissions,
_ => null
};
if (missions is not null)
{
if (!MissionIdentifier.IsEmpty)
{
return missions.Any(m => m.Prefab.Identifier == MissionIdentifier);
}
else if (!MissionTag.IsEmpty)
{
return missions.Count(m => m.Prefab.Tags.Contains(MissionTag.Value)) >= MissionCount;
}
else
{
return missions.Count() >= MissionCount;
}
}
return MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionCount == 0;
}
}
@@ -0,0 +1,15 @@
namespace Barotrauma;
partial class CheckObjectiveAction : BinaryOptionAction
{
public CheckObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
bool success = false;
DetermineSuccessProjSpecific(ref success);
return success;
}
partial void DetermineSuccessProjSpecific(ref bool success);
}
@@ -1,7 +1,15 @@
using Barotrauma.Extensions;
namespace Barotrauma
{
class CheckOrderAction : BinaryOptionAction
{
public enum OrderPriority
{
Top,
Any
}
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
@@ -14,35 +22,58 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier OrderTargetTag { get; set; }
[Serialize(OrderPriority.Top, IsPropertySaveable.Yes)]
public OrderPriority Priority { get; set; }
public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
Character targetCharacter = null;
if (!TargetTag.IsEmpty)
var targetCharacters = ParentEvent.GetTargets(TargetTag);
if (targetCharacters.None())
{
foreach (var t in ParentEvent.GetTargets(TargetTag))
{
if (t is Character c)
{
targetCharacter = c;
break;
}
}
}
if (targetCharacter == null)
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target character was found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
return false;
}
var currentOrderInfo = targetCharacter.GetCurrentOrderWithTopPriority();
if (currentOrderInfo?.Identifier == OrderIdentifier)
foreach (var t in targetCharacters)
{
if (!OrderTargetTag.IsEmpty)
if (t is not Character c)
{
if (currentOrderInfo.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag)) { return false; }
continue;
}
if (Priority == OrderPriority.Top)
{
if (c.GetCurrentOrderWithTopPriority() is Order topPrioOrder && IsMatch(topPrioOrder))
{
return true;
}
}
else if (Priority == OrderPriority.Any)
{
foreach (var order in c.CurrentOrders)
{
if (IsMatch(order))
{
return true;
}
}
}
bool IsMatch(Order order)
{
if (order?.Identifier == OrderIdentifier)
{
if (!OrderTargetTag.IsEmpty && (order.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag)))
{
return false;
}
if (OrderOption.IsEmpty || order?.Option == OrderOption)
{
return true;
}
}
return false;
}
return OrderOption.IsEmpty || currentOrderInfo?.Option == OrderOption;
}
return false;
}
@@ -0,0 +1,87 @@
using System;
using System.Linq;
namespace Barotrauma;
class CheckPurchasedItemsAction : BinaryOptionAction
{
public enum TransactionType
{
Purchased,
Sold
}
[Serialize(TransactionType.Purchased, IsPropertySaveable.Yes)]
public TransactionType Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemTag { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
public int MinCount { get; set; }
public CheckPurchasedItemsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
MinCount = Math.Max(MinCount, 1);
}
protected override bool? DetermineSuccess()
{
if (ItemIdentifier.IsEmpty && ItemTag.IsEmpty)
{
return false;
}
if (GameMain.GameSession?.Campaign?.CargoManager is not CargoManager cargoManager)
{
return false;
}
if (Type == TransactionType.Purchased)
{
int totalPurchased = 0;
foreach ((Identifier id, var items) in cargoManager.PurchasedItems)
{
if (!ItemIdentifier.IsEmpty)
{
totalPurchased += items.Find(i => i.ItemPrefabIdentifier == ItemIdentifier)?.Quantity ?? 0;
}
else if (!ItemTag.IsEmpty)
{
foreach (var item in items)
{
if (item.ItemPrefab.Tags.Contains(ItemTag))
{
totalPurchased += item.Quantity;
}
}
}
if (totalPurchased >= MinCount)
{
return true;
}
}
}
else
{
int totalSold = 0;
foreach ((Identifier id, var items) in cargoManager.SoldItems)
{
if (!ItemIdentifier.IsEmpty)
{
totalSold += items.Count(i => i.ItemPrefab.Identifier == ItemIdentifier);
}
else if (!ItemTag.IsEmpty)
{
totalSold += items.Count(i => i.ItemPrefab.Tags.Contains(ItemTag));
}
if (totalSold >= MinCount)
{
return true;
}
}
}
return false;
}
}
@@ -59,7 +59,10 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes)]
public bool ContinueConversation { get; set; }
public Character speaker
[Serialize(false, IsPropertySaveable.Yes)]
public bool IgnoreInterruptDistance { get; set; }
public Character Speaker
{
get;
private set;
@@ -124,7 +127,7 @@ namespace Barotrauma
#else
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.InGame && c.Character != null) { ServerWrite(speaker, c, interrupt); }
if (c.InGame && c.Character != null) { ServerWrite(Speaker, c, interrupt); }
}
#endif
ResetSpeaker();
@@ -160,7 +163,7 @@ namespace Barotrauma
selectedOption = -1;
interrupt = false;
dialogOpened = false;
speaker = null;
Speaker = null;
}
public override bool SetGoToTarget(string goTo)
@@ -181,15 +184,14 @@ namespace Barotrauma
private void ResetSpeaker()
{
if (speaker == null) { return; }
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
speaker.ActiveConversation = null;
speaker.SetCustomInteract(null, null);
if (Speaker == null) { return; }
Speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
Speaker.ActiveConversation = null;
Speaker.SetCustomInteract(null, null);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData());
GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData());
#endif
var humanAI = speaker.AIController as HumanAIController;
if (humanAI != null && !speaker.IsDead && !speaker.Removed)
if (Speaker.AIController is HumanAIController humanAI && !Speaker.IsDead && !Speaker.Removed)
{
humanAI.ClearForcedOrder();
if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); }
@@ -207,7 +209,6 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
lastActiveTime = Timing.TotalTime;
if (interrupt)
{
Interrupted?.Update(deltaTime);
@@ -216,6 +217,7 @@ namespace Barotrauma
{
if (dialogOpened)
{
lastActiveTime = Timing.TotalTime;
#if CLIENT
if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction"))
{
@@ -226,7 +228,7 @@ namespace Barotrauma
Reset();
}
#endif
if (ShouldInterrupt())
if (ShouldInterrupt(requireTarget: true))
{
ResetSpeaker();
interrupt = true;
@@ -236,34 +238,34 @@ namespace Barotrauma
if (!SpeakerTag.IsEmpty)
{
if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; }
speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character;
if (speaker == null || speaker.Removed)
if (Speaker != null && !Speaker.Removed && Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; }
Speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character;
if (Speaker == null || Speaker.Removed)
{
return;
}
//some conversation already assigned to the speaker, wait for it to be removed
if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent)
if (Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent)
{
return;
}
else if (!WaitForInteraction)
{
TryStartConversation(speaker);
TryStartConversation(Speaker);
}
else if (speaker.ActiveConversation != this)
else if (Speaker.ActiveConversation != this)
{
speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk;
speaker.ActiveConversation = this;
Speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk;
Speaker.ActiveConversation = this;
#if CLIENT
speaker.SetCustomInteract(
Speaker.SetCustomInteract(
TryStartConversation,
TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)));
#else
speaker.SetCustomInteract(
Speaker.SetCustomInteract(
TryStartConversation,
TextManager.Get("CampaignInteraction.Talk"));
GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData());
GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData());
#endif
}
return;
@@ -275,7 +277,9 @@ namespace Barotrauma
}
else
{
if (ShouldInterrupt())
//after the conversation has been finished and the target character assigned,
//we no longer care if we still have a target
if (ShouldInterrupt(requireTarget: false))
{
ResetSpeaker();
interrupt = true;
@@ -287,35 +291,36 @@ namespace Barotrauma
}
}
private bool ShouldInterrupt()
private bool ShouldInterrupt(bool requireTarget)
{
IEnumerable<Entity> targets = Enumerable.Empty<Entity>();
if (!TargetTag.IsEmpty)
if (!TargetTag.IsEmpty && requireTarget)
{
targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e));
targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e, requireTarget));
if (!targets.Any()) { return true; }
}
if (speaker != null)
if (Speaker != null)
{
if (!TargetTag.IsEmpty)
if (!TargetTag.IsEmpty && requireTarget && !IgnoreInterruptDistance)
{
if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; }
if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, Speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; }
}
if (speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction())
if (Speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction())
{
return true;
}
return speaker.Removed || speaker.IsDead || speaker.IsIncapacitated;
return Speaker.Removed || Speaker.IsDead || Speaker.IsIncapacitated;
}
return false;
}
private bool IsValidTarget(Entity e)
private bool IsValidTarget(Entity e, bool requirePlayerControlled = true)
{
bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
(e == Character.Controlled || character.IsRemotePlayer);
bool isValid =
e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
(character == Character.Controlled || character.IsRemotePlayer || !requirePlayerControlled);
#if SERVER
if (!dialogOpened)
{
@@ -49,6 +49,12 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ObjectiveTag { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
public bool ObjectiveCanBeCompleted { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ParentObjectiveId { get; set; }
private bool isFinished = false;
public MessageBoxAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
@@ -67,7 +67,7 @@ namespace Barotrauma
void ChangeItemTeam(Submarine sub, bool allowStealing)
{
foreach (Item item in npc.Inventory.AllItems)
foreach (Item item in npc.Inventory.FindAllItems(recursive: true))
{
item.AllowStealing = allowStealing;
if (item.GetComponent<Items.Components.WifiComponent>() is { } wifiComponent)
@@ -1,9 +1,8 @@
using System;
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -11,6 +10,7 @@ namespace Barotrauma
{
public enum SpawnLocationType
{
Any,
MainSub,
Outpost,
MainPath,
@@ -40,7 +40,7 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Tag of an entity with an inventory to spawn the item into.")]
public Identifier TargetInventory { get; set; }
[Serialize(SpawnLocationType.MainSub, IsPropertySaveable.Yes)]
[Serialize(SpawnLocationType.Any, IsPropertySaveable.Yes)]
public SpawnLocationType SpawnLocation { get; set; }
[Serialize(SpawnType.Human, IsPropertySaveable.Yes)]
@@ -177,7 +177,7 @@ namespace Barotrauma
}
else if (!ItemIdentifier.IsEmpty)
{
if (!(MapEntityPrefab.FindByIdentifier(ItemIdentifier) is ItemPrefab itemPrefab))
if (MapEntityPrefab.FindByIdentifier(ItemIdentifier) is not ItemPrefab itemPrefab)
{
DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)");
}
@@ -275,6 +275,7 @@ namespace Barotrauma
{
return spawnLocation switch
{
SpawnLocationType.Any => true,
SpawnLocationType.MainSub => submarine == Submarine.MainSub,
SpawnLocationType.MainPath => submarine == null,
SpawnLocationType.Outpost => submarine is { Info: { IsOutpost: true } },
@@ -32,11 +32,13 @@ namespace Barotrauma
("bot", v => TagBots(playerCrewOnly: false)),
("crew", v => TagCrew()),
("humanprefabidentifier", TagHumansByIdentifier),
("jobidentifier", TagHumansByJobIdentifier),
("structureidentifier", TagStructuresByIdentifier),
("structurespecialtag", TagStructuresBySpecialTag),
("itemidentifier", TagItemsByIdentifier),
("itemtag", TagItemsByTag),
("hullname", TagHullsByName)
("hullname", TagHullsByName),
("submarine", TagSubmarinesByType),
}.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary();
}
@@ -93,6 +95,18 @@ namespace Barotrauma
}
}
}
private void TagHumansByJobIdentifier(Identifier jobIdentifier)
{
foreach (Character c in Character.CharacterList)
{
if (c.HasJob(jobIdentifier))
{
ParentEvent.AddTarget(Tag, c);
}
}
}
private void TagStructuresByIdentifier(Identifier identifier)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier);
@@ -118,6 +132,11 @@ namespace Barotrauma
ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase));
}
private void TagSubmarinesByType(Identifier type)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier()));
}
private bool SubmarineTypeMatches(Submarine sub)
{
if (SubmarineType == SubType.Any) { return true; }
@@ -8,6 +8,12 @@ namespace Barotrauma
{
class TriggerAction : EventAction
{
public enum TriggerType
{
Inside,
Outside
}
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the first entity that will be used for trigger checks.")]
public Identifier Target1Tag { get; set; }
@@ -23,7 +29,10 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the second entity when the trigger check succeeds.")]
public Identifier ApplyToTarget2 { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Range both entities must be within to activate the trigger.")]
[Serialize(TriggerType.Inside, IsPropertySaveable.Yes, description: "Determines if the targets must be inside or outside of the radius.")]
public TriggerType Type { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Range to activate the trigger.")]
public float Radius { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")]
@@ -38,6 +47,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")]
public bool AllowMultipleTargets { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If true and using multiple targets, all targets must be inside/outside the radius.")]
public bool CheckAllTargets { get; set; }
private float distance;
public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -57,6 +69,8 @@ namespace Barotrauma
public bool isRunning = false;
private readonly List<Either<Character, Item>> npcsOrItems = new List<Either<Character, Item>>();
private readonly List<(Entity e1, Entity e2)> triggerers = new List<(Entity e1, Entity e2)>();
public override void Update(float deltaTime)
{
@@ -66,18 +80,44 @@ namespace Barotrauma
var targets1 = ParentEvent.GetTargets(Target1Tag);
if (!targets1.Any()) { return; }
triggerers.Clear();
foreach (Entity e1 in targets1)
{
if (DisableInCombat && IsInCombat(e1)) { continue; }
if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) { continue; }
if (DisableInCombat && IsInCombat(e1))
{
if (CheckAllTargets)
{
return;
}
continue;
}
if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated))
{
if (CheckAllTargets)
{
return;
}
continue;
}
if (!TargetModuleType.IsEmpty)
{
if (IsCloseEnoughToHull(e1, out Hull hull))
if (!CheckAllTargets && CheckDistanceToHull(e1, out Hull hull))
{
Trigger(e1, hull);
return;
}
else if (CheckAllTargets)
{
if (CheckDistanceToHull(e1, out hull))
{
triggerers.Add((e1, hull));
}
else
{
return;
}
}
continue;
}
@@ -85,9 +125,26 @@ namespace Barotrauma
foreach (Entity e2 in targets2)
{
if (e1 == e2) { continue; }
if (DisableInCombat && IsInCombat(e2)) { continue; }
if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) { continue; }
if (e1 == e2)
{
continue;
}
if (DisableInCombat && IsInCombat(e2))
{
if (CheckAllTargets)
{
return;
}
continue;
}
if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated))
{
if (CheckAllTargets)
{
return;
}
continue;
}
if (WaitForInteraction)
{
@@ -173,16 +230,35 @@ namespace Barotrauma
Vector2 pos1 = e1.WorldPosition;
Vector2 pos2 = e2.WorldPosition;
distance = Vector2.Distance(pos1, pos2);
if (((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) ||
((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) ||
Vector2.DistanceSquared(pos1, pos2) < Radius * Radius)
if ((Type == TriggerType.Inside) == IsWithinRadius())
{
if (!CheckAllTargets)
{
Trigger(e1, e2);
return;
}
else
{
triggerers.Add((e1, e2));
}
}
else if (CheckAllTargets)
{
Trigger(e1, e2);
return;
}
bool IsWithinRadius() =>
((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) ||
((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) ||
Vector2.DistanceSquared(pos1, pos2) < Radius * Radius;
}
}
}
}
foreach (var (e1, e2) in triggerers)
{
Trigger(e1, e2);
}
}
private void ResetTargetIcons()
@@ -205,7 +281,7 @@ namespace Barotrauma
}
}
private bool IsCloseEnoughToHull(Entity e, out Hull hull)
private bool CheckDistanceToHull(Entity e, out Hull hull)
{
hull = null;
if (Radius <= 0)
@@ -213,36 +289,35 @@ namespace Barotrauma
if (e is Character character && character.CurrentHull != null && character.CurrentHull.OutpostModuleTags.Contains(TargetModuleType))
{
hull = character.CurrentHull;
return true;
return Type == TriggerType.Inside;
}
else if (e is Item item && item.CurrentHull != null && item.CurrentHull.OutpostModuleTags.Contains(TargetModuleType))
{
hull = item.CurrentHull;
return true;
return Type == TriggerType.Inside;
}
return false;
return Type == TriggerType.Outside;
}
else
{
foreach (Hull potentialHull in Hull.HullList)
{
if (!potentialHull.OutpostModuleTags.Contains(TargetModuleType)) { continue; }
Rectangle hullRect = potentialHull.WorldRect;
hullRect.Inflate(Radius, Radius);
if (Submarine.RectContains(hullRect, e.WorldPosition))
{
hull = potentialHull;
return true;
return Type == TriggerType.Inside;
}
}
return false;
return Type == TriggerType.Outside;
}
}
private bool IsInCombat(Entity entity)
private static bool IsInCombat(Entity entity)
{
if (!(entity is Character character)) { return false; }
if (entity is not Character character) { return false; }
foreach (Character c in Character.CharacterList)
{
if (c.IsDead || c.Removed || c.IsIncapacitated || !c.Enabled) { continue; }
@@ -13,6 +13,12 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ObjectiveTag { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
public bool CanBeCompleted { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ParentObjectiveId { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool AutoPlayVideo { get; set; }
@@ -73,6 +73,7 @@ namespace Barotrauma
private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
private readonly HashSet<EventPrefab> nonRepeatableEvents = new HashSet<EventPrefab>();
private readonly HashSet<EventSet> usedUniqueSets = new HashSet<EventSet>();
#if DEBUG && SERVER
@@ -155,12 +156,19 @@ namespace Barotrauma
}
rand = new MTRandom(seed);
EventSet initialEventSet = SelectRandomEvents(EventSet.Prefabs.ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand);
bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode;
EventSet initialEventSet = SelectRandomEvents(
EventSet.Prefabs.ToList(),
requireCampaignSet: playingCampaign,
random: rand);
EventSet additiveSet = null;
if (initialEventSet != null && initialEventSet.Additive)
{
additiveSet = initialEventSet;
initialEventSet = SelectRandomEvents(EventSet.Prefabs.Where(e => !e.Additive).ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand);
initialEventSet = SelectRandomEvents(
EventSet.Prefabs.Where(e => !e.Additive).ToList(),
requireCampaignSet: playingCampaign,
random: rand);
}
if (initialEventSet != null)
{
@@ -201,6 +209,7 @@ namespace Barotrauma
}
AddChildEvents(initialEventSet);
void AddChildEvents(EventSet eventSet)
{
if (eventSet == null) { return; }
@@ -351,6 +360,7 @@ namespace Barotrauma
QueuedEvents.Clear();
finishedEvents.Clear();
nonRepeatableEvents.Clear();
usedUniqueSets.Clear();
preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
@@ -364,15 +374,25 @@ namespace Barotrauma
/// </summary>
public void RegisterEventHistory()
{
level.LevelData.EventsExhausted = true;
if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost)
if (level?.LevelData != null)
{
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
level.LevelData.EventsExhausted = true;
if (level.LevelData.Type == LevelData.LevelType.Outpost)
{
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
{
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e)));
}
foreach (var usedUniqueSet in usedUniqueSets)
{
if (!level.LevelData.UsedUniqueSets.Contains(usedUniqueSet.Identifier))
{
level.LevelData.UsedUniqueSets.Add(usedUniqueSet.Identifier);
}
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e)));
}
}
@@ -398,6 +418,11 @@ namespace Barotrauma
DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true);
if (eventSet.Unique && !usedUniqueSets.Contains(eventSet))
{
usedUniqueSets.Add(eventSet);
}
int applyCount = 1;
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
if (eventSet.PerRuin)
@@ -496,12 +521,12 @@ namespace Barotrauma
selectedEvents[eventSet].Add(newEvent);
}
Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation;
var location = GetEventLocation();
foreach (EventSet childEventSet in eventSet.ChildSets)
{
if (!IsValidForLevel(childEventSet, level)) { continue; }
if (location != null && !IsValidForLocation(childEventSet, location)) { continue; }
CreateEvents(childEventSet);
if (!IsValidForLocation(childEventSet, location)) { continue; }
CreateEvents(childEventSet);
}
}
}
@@ -536,10 +561,32 @@ namespace Barotrauma
}
}
Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation;
if (location != null)
var location = GetEventLocation();
allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location));
allowedEventSets = allowedEventSets.Where(set => !set.CampaignTutorialOnly ||
(GameMain.IsSingleplayer && GameMain.GameSession?.Campaign?.Settings is { TutorialEnabled: true }));
int? discoveryIndex = GameMain.GameSession?.Map?.GetDiscoveryIndex(location);
int? visitIndex = GameMain.GameSession?.Map?.GetVisitIndex(location);
if (discoveryIndex is not null && discoveryIndex >= 0 && allowedEventSets.Any(set => set.ForceAtDiscoveredNr == discoveryIndex))
{
allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location));
allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr == discoveryIndex);
}
else if (visitIndex is not null && visitIndex >= 0 && allowedEventSets.Any(set => set.ForceAtVisitedNr == visitIndex))
{
allowedEventSets = allowedEventSets.Where(set => set.ForceAtVisitedNr == visitIndex);
}
else
{
// When there are no forced sets, only allow sets that aren't forced at any specific location
allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr < 0 && set.ForceAtVisitedNr < 0);
}
if (allowedEventSets.Count() == 1)
{
// When there's only a single set available, just select it directly
return allowedEventSets.First();
}
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
@@ -558,18 +605,27 @@ namespace Barotrauma
return null;
}
private bool IsValidForLevel(EventSet eventSet, Level level)
private static bool IsValidForLevel(EventSet eventSet, Level level)
{
return
level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty &&
level.LevelData.Type == eventSet.LevelType &&
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier);
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier) &&
(!eventSet.Unique || !level.LevelData.UsedUniqueSets.Contains(eventSet.Identifier));
}
private bool IsValidForLocation(EventSet eventSet, Location location)
{
return eventSet.LocationTypeIdentifiers == null ||
eventSet.LocationTypeIdentifiers.Any(identifier => identifier == location.GetLocationType().Identifier);
if (location is null) { return true; }
var locationType = location.GetLocationType();
bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents;
if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; }
return eventSet.LocationTypeIdentifiers != null && eventSet.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier);
}
private Location GetEventLocation()
{
return GameMain.GameSession?.Campaign?.Map?.CurrentLocation ?? level?.StartLocation;
}
private bool CanStartEventSet(EventSet eventSet)
@@ -113,8 +113,17 @@ namespace Barotrauma
public readonly bool PerRuin, PerCave, PerWreck;
public readonly bool DisableInHuntingGrounds;
/// <summary>
/// If true, events from this set shouldn't be selected again as long as they remain in <see cref="LevelData.NonRepeatableEvents"/> which has a limited size.
/// Use <see cref="Unique"/> to prevent selecting the whole set again altogether.
/// </summary>
public readonly bool OncePerOutpost;
/// <summary>
/// If true, the whole set can only be selected once for a level.
/// </summary>
public readonly bool Unique;
public readonly bool DelayWhenCrewAway;
public readonly bool TriggerEventCooldown;
@@ -126,6 +135,18 @@ namespace Barotrauma
public readonly float ResetTime;
/// <summary>
/// Used to force an event set based on how many other locations have been discovered before this. (Used for campaign tutorial event sets.)
/// </summary>
public readonly int ForceAtDiscoveredNr;
/// <summary>
/// Used to force an event set based on how many other outposts have been visited before this. (Used for campaign tutorial event sets.)
/// </summary>
public readonly int ForceAtVisitedNr;
public readonly bool CampaignTutorialOnly;
public readonly struct SubEventPrefab
{
public SubEventPrefab(Either<Identifier[], EventPrefab> prefabOrIdentifiers, float? commonness, float? probability)
@@ -269,9 +290,18 @@ namespace Barotrauma
IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck));
DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck);
OncePerOutpost = element.GetAttributeBool("onceperoutpost", false);
Unique = element.GetAttributeBool("unique", false);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false));
ResetTime = element.GetAttributeFloat("resettime", 0);
CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), false);
ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1);
ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1);
if (ForceAtDiscoveredNr >= 0 && ForceAtVisitedNr >= 0)
{
DebugConsole.ThrowError($"Error with event set \"{Identifier}\" - both ForceAtDiscoveredNr and ForceAtVisitedNr are defined, this could lead to unexpected behavior");
}
DefaultCommonness = element.GetAttributeFloat("commonness", 1.0f);
foreach (var subElement in element.Elements())
@@ -489,6 +519,11 @@ namespace Barotrauma
}
}
public override string ToString()
{
return $"{base.ToString()} ({Identifier.Value})";
}
public override void Dispose() { }
}
}
@@ -378,7 +378,7 @@ namespace Barotrauma
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
// use multipliers here so that we can easily add them together without introducing multiplicative XP stacking
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier));
crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));
@@ -386,13 +386,20 @@ namespace Barotrauma
#if CLIENT
foreach (Character character in crewCharacters)
{
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
character.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
character.Info?.GiveExperience(experienceGain, isMissionExperience: true);
}
#else
foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients)
{
//give the experience to the stored characterinfo if the client isn't currently controlling a character
(c.Character?.Info ?? c.CharacterInfo)?.GiveExperience(experienceGain, isMissionExperience: true);
CharacterInfo info = c.Character?.Info ?? c.CharacterInfo;
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value), isMissionExperience: true);
}
#endif
@@ -619,4 +626,16 @@ namespace Barotrauma
public Mission Mission { get; set; }
}
class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission
{
public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier)
{
Value = missionExperienceGainMultiplier;
Mission = mission;
}
public float Value { get; set; }
public Mission Mission { get; set; }
}
}
@@ -218,6 +218,8 @@ namespace Barotrauma.Extensions
return new Dictionary<TKey, TValue>(immutableDictionary);
}
public static NetCollection<T> ToNetCollection<T>(this IEnumerable<T> enumerable) => new NetCollection<T>(enumerable.ToImmutableArray());
/// <summary>
/// Returns whether a given collection has at least a certain amount
/// of elements for which the predicate returns true.
@@ -1,10 +1,15 @@
#nullable enable
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
public enum FactionAffiliation
{
Affiliated,
Neutral
}
class Faction
{
public Reputation Reputation { get; }
@@ -16,11 +21,25 @@ namespace Barotrauma
Reputation = new Reputation(metadata, this, prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation);
}
public bool IsAffiliated()
/// <summary>
/// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents
/// </summary>
/// <returns></returns>
public FactionAffiliation GetPlayerAffiliationStatus()
{
if (GameMain.GameSession?.Campaign?.Factions.MaxBy(static f => f.Reputation.Value) is not { } highestFaction) { return false; }
float affiliation = 1f;
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (character.Info is not { } info) { continue; }
return highestFaction.Reputation.Value < 0 || Prefab.Identifier == highestFaction.Prefab.Identifier;
affiliation *= 1f + info.GetSavedStatValue(StatTypes.Affiliation, Prefab.Identifier);
}
return affiliation switch
{
>= 1f => FactionAffiliation.Affiliated,
_ => FactionAffiliation.Neutral
};
}
}
@@ -749,6 +749,7 @@ namespace Barotrauma
location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty);
location.Reset();
}
Map.ClearLocationHistory();
Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation));
Map.SelectLocation(-1);
if (Map.Radiation != null)
@@ -18,6 +18,9 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public string PresetName { get; set; } = string.Empty;
[Serialize(true, IsPropertySaveable.Yes)]
public bool TutorialEnabled { get; set; }
[Serialize(false, IsPropertySaveable.Yes), NetworkSerialize]
public bool RadiationEnabled { get; set; }
@@ -104,7 +107,9 @@ namespace Barotrauma
private static int GetAddedMissionCount()
{
return GameSession.GetSessionCrewCharacters(CharacterType.Both).Max(static character => (int)character.GetStatValue(StatTypes.ExtraMissionCount));
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (!characters.Any()) { return 0; }
return characters.Max(static character => (int)character.GetStatValue(StatTypes.ExtraMissionCount));
}
}
}
@@ -133,13 +133,13 @@ namespace Barotrauma
}
partial void InitProjSpecific();
public static string GetCharacterDataSavePath(string savePath)
{
return Path.Combine(SaveUtil.MultiplayerSaveFolder, Path.GetFileNameWithoutExtension(savePath) + "_CharacterData.xml");
return Path.Combine(Path.GetDirectoryName(savePath), Path.GetFileNameWithoutExtension(savePath) + "_CharacterData.xml");
}
public string GetCharacterDataSavePath()
public static string GetCharacterDataSavePath()
{
return GetCharacterDataSavePath(GameMain.GameSession.SavePath);
}
@@ -29,6 +29,14 @@ namespace Barotrauma
public readonly Sprite Banner;
public readonly EndMessageInfo EndMessage;
public enum EndType { None, Continue, Restart }
public readonly record struct EndMessageInfo(
EndType EndType,
Identifier NextTutorialIdentifier);
public TutorialPrefab(ContentFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
Order = element.GetAttributeInt("order", int.MaxValue);
@@ -59,6 +67,13 @@ namespace Barotrauma
}
EventIdentifier = element.GetChildElement("scriptedevent")?.GetAttributeIdentifier("identifier", "") ?? Identifier.Empty;
if (element.GetChildElement("endmessage") is ContentXElement endMessageElement)
{
EndMessage = new EndMessageInfo(
EndType: endMessageElement.GetAttributeEnum("type", EndType.None),
NextTutorialIdentifier: endMessageElement.GetAttributeIdentifier("nexttutorial", Identifier.Empty));
}
}
public CharacterInfo GetTutorialCharacterInfo()
@@ -582,6 +582,9 @@ namespace Barotrauma
}
}
#if CLIENT
ObjectiveManager.ResetObjectives();
#endif
EventManager?.StartRound(Level.Loaded);
SteamAchievementManager.OnStartRound();
@@ -847,6 +850,7 @@ namespace Barotrauma
if (GameMain.NetLobbyScreen != null) { GameMain.NetLobbyScreen.OnRoundEnded(); }
TabMenu.OnRoundEnded();
GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb));
ObjectiveManager.ResetUI();
#endif
SteamAchievementManager.OnRoundEnded(this);
@@ -9,7 +9,7 @@ using Barotrauma.Networking;
namespace Barotrauma
{
internal partial class MedicalClinic
internal sealed partial class MedicalClinic
{
public enum NetworkHeader
{
@@ -18,7 +18,8 @@ namespace Barotrauma
ADD_PENDING,
REMOVE_PENDING,
CLEAR_PENDING,
HEAL_PENDING
HEAL_PENDING,
ADD_EVERYTHING_TO_PENDING
}
public enum AfflictionSeverity
@@ -43,23 +44,10 @@ namespace Barotrauma
}
[NetworkSerialize]
public struct NetHealRequest : INetSerializableStruct
{
public HealRequestResult Result;
}
public readonly record struct NetHealRequest(HealRequestResult Result) : INetSerializableStruct;
[NetworkSerialize]
public struct NetRemovedAffliction : INetSerializableStruct
{
public NetCrewMember CrewMember;
public NetAffliction Affliction;
}
public struct NetPendingCrew : INetSerializableStruct
{
[NetworkSerialize(ArrayMaxSize = CrewManager.MaxCrewSize)]
public NetCrewMember[] CrewMembers;
}
public readonly record struct NetRemovedAffliction(NetCrewMember CrewMember, NetAffliction Affliction) : INetSerializableStruct;
public struct NetAffliction : INetSerializableStruct
{
@@ -87,7 +75,7 @@ namespace Barotrauma
}
// between 0.1 and 0.5
if (normalizedStrength > 0.1f && normalizedStrength < 0.5f)
if (normalizedStrength is > 0.1f and < 0.5f)
{
return AfflictionSeverity.Medium;
}
@@ -146,17 +134,23 @@ namespace Barotrauma
}
}
public struct NetCrewMember : INetSerializableStruct
public record struct NetCrewMember : INetSerializableStruct
{
[NetworkSerialize]
public int CharacterInfoID;
[NetworkSerialize]
public NetAffliction[] Afflictions;
public ImmutableArray<NetAffliction> Afflictions;
public CharacterInfo CharacterInfo
public NetCrewMember(CharacterInfo info)
{
set => CharacterInfoID = value.GetIdentifierUsingOriginalName();
CharacterInfoID = info.GetIdentifierUsingOriginalName();
Afflictions = ImmutableArray<NetAffliction>.Empty;
}
public NetCrewMember(CharacterInfo info, ImmutableArray<NetAffliction> afflictions): this(info)
{
Afflictions = afflictions;
}
public readonly CharacterInfo? FindCharacterInfo(ImmutableArray<CharacterInfo> crew)
@@ -194,11 +188,11 @@ namespace Barotrauma
private static bool IsOutpostInCombat()
{
if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; }
if (Level.Loaded is not { Type: LevelData.LevelType.Outpost }) { return false; }
IEnumerable<Character> crew = GetCrewCharacters().Where(c => c.Character != null).Select(c => c.Character).ToImmutableHashSet();
IEnumerable<Character> crew = GetCrewCharacters().Where(static c => c.Character != null).Select(static c => c.Character).ToImmutableHashSet();
foreach (Character npc in Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.FriendlyNPC))
foreach (Character npc in Character.CharacterList.Where(static c => c.TeamID == CharacterTeamType.FriendlyNPC))
{
bool isInCombatWithCrew = !npc.IsInstigator && npc.AIController is HumanAIController { ObjectiveManager: { CurrentObjective: AIObjectiveCombat combatObjective } } && crew.Contains(combatObjective.Enemy);
if (isInCombatWithCrew) { return true; }
@@ -238,6 +232,20 @@ namespace Barotrauma
PendingHeals.Clear();
}
private void AddEverythingToPending()
{
foreach (CharacterInfo info in GetCrewCharacters())
{
if (info.Character?.CharacterHealth is not { } health) { continue; }
var afflictions = GetAllAfflictions(health);
if (afflictions.Length is 0) { continue; }
InsertPendingCrewMember(new NetCrewMember(info, afflictions));
}
}
private void RemovePendingAffliction(NetCrewMember crewMember, NetAffliction affliction)
{
foreach (NetCrewMember listMember in PendingHeals.ToList())
@@ -255,7 +263,7 @@ namespace Barotrauma
newAfflictions.Add(pendingAffliction);
}
pendingMember.Afflictions = newAfflictions.ToArray();
pendingMember.Afflictions = newAfflictions.ToImmutableArray();
}
if (!pendingMember.Afflictions.Any()) { continue; }
@@ -280,9 +288,9 @@ namespace Barotrauma
static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold));
}
private NetAffliction[] GetAllAfflictions(CharacterHealth health)
private ImmutableArray<NetAffliction> GetAllAfflictions(CharacterHealth health)
{
IEnumerable<Affliction> rawAfflictions = health.GetAllAfflictions().Where(a => IsHealable(a));
IEnumerable<Affliction> rawAfflictions = health.GetAllAfflictions().Where(IsHealable);
List<NetAffliction> afflictions = new List<NetAffliction>();
@@ -305,12 +313,12 @@ namespace Barotrauma
afflictions.Add(newAffliction);
}
return afflictions.ToArray();
return afflictions.ToImmutableArray();
static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength));
}
public int GetTotalCost() => PendingHeals.SelectMany(h => h.Afflictions).Aggregate(0, (current, affliction) => current + affliction.Price);
public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price);
private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue;
@@ -325,7 +333,7 @@ namespace Barotrauma
}
#endif
return Character.CharacterList.Where(c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(c => c.Info).ToImmutableArray();
return Character.CharacterList.Where(static c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(static c => c.Info).ToImmutableArray();
}
#if DEBUG && CLIENT
@@ -493,6 +493,10 @@ namespace Barotrauma
base.PutItem(item, i, user, removeItem, createNetworkEvent);
#if CLIENT
CreateSlots();
if (character == Character.Controlled)
{
HintManager.OnObtainedItem(character, item);
}
#endif
if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo)
{
@@ -843,10 +843,18 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (item.body == null || !item.body.Enabled) { return; }
Character owner = picker ?? item.GetRootInventoryOwner() as Character;
if (owner != null)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, owner);
}
if (picker == null || !picker.HasEquippedItem(item))
{
if (Pusher != null) { Pusher.Enabled = false; }
if (attachTargetCell == null) { IsActive = false; }
if (attachTargetCell == null && owner == null) { IsActive = false; }
return;
}
@@ -855,23 +863,7 @@ namespace Barotrauma.Items.Components
Drawable = true;
}
Vector2 swing = Vector2.Zero;
if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f)
{
swingState += deltaTime;
swingState %= 1.0f;
if (SwingWhenHolding ||
(SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) ||
(SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot)))
{
swing = swingAmount * new Vector2(
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f,
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f);
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
UpdateSwingPos(deltaTime, out Vector2 swingPos);
if (item.body.Dir != picker.AnimController.Dir)
{
item.FlipX(relativeToSub: false);
@@ -884,7 +876,7 @@ 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 + swing, aimPos + swing, aim, holdAngle);
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle);
if (!aim)
{
var rope = GetRope();
@@ -921,6 +913,24 @@ namespace Barotrauma.Items.Components
}
}
public void UpdateSwingPos(float deltaTime, out Vector2 swingPos)
{
swingPos = Vector2.Zero;
if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f)
{
swingState += deltaTime;
swingState %= 1.0f;
if (SwingWhenHolding ||
(SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) ||
(SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot)))
{
swingPos = swingAmount * new Vector2(
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f,
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f);
}
}
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
//do nothing
@@ -214,8 +214,9 @@ namespace Barotrauma.Items.Components
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim;
if (aim)
{
UpdateSwingPos(deltaTime, out Vector2 swingPos);
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
if (ac.InWater)
{
ac.LockFlippingUntil = (float)Timing.TotalTime + Reload;
@@ -392,34 +393,35 @@ namespace Barotrauma.Items.Components
float damageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier);
damageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.StrikingPowerMultiplier);
Character user = User;
Limb targetLimb = target.UserData as Limb;
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
if (Attack != null)
{
Attack.SetUser(User);
Attack.SetUser(user);
Attack.DamageMultiplier = damageMultiplier;
if (targetLimb != null)
{
if (targetLimb.character.Removed) { return; }
targetLimb.character.LastDamageSource = item;
Attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f);
Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f);
}
else if (targetCharacter != null)
{
if (targetCharacter.Removed) { return; }
targetCharacter.LastDamageSource = item;
Attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f);
Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
}
else if ((target.UserData as Structure ?? targetFixture.UserData as Structure) is Structure targetStructure)
{
if (targetStructure.Removed) { return; }
Attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f);
Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
}
else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
{
if (targetItem.Removed) { return; }
var attackResult = Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar)
{
@@ -435,7 +437,7 @@ namespace Barotrauma.Items.Components
else if (target.UserData is Holdable holdable && holdable.CanPush)
{
if (holdable.Item.Removed) { return; }
Attack.DoDamage(User, holdable.Item, item.WorldPosition, 1.0f);
Attack.DoDamage(user, holdable.Item, item.WorldPosition, 1.0f);
RestoreCollision();
hitting = false;
User = null;
@@ -448,29 +450,32 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
bool success = Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(User);
#if SERVER
if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits
ActionType conditionalActionType = ActionType.OnSuccess;
if (user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(user))
{
GameMain.Server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(
success ? ActionType.OnUse : ActionType.OnFailure,
targetItemComponent: null,
targetCharacter, targetLimb));
string logStr = picker?.LogName + " used " + item.Name;
if (item.ContainedItems != null && item.ContainedItems.Any())
{
logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")";
}
logStr += " on " + targetCharacter.LogName + ".";
Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack);
conditionalActionType = ActionType.OnFailure;
}
if (GameMain.NetworkMember is { IsServer: true } server && targetCharacter != null)
{
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb));
#if SERVER
if (GameMain.Server != null) //TODO: Log structure hits
{
string logStr = picker?.LogName + " used " + item.Name;
if (item.ContainedItems != null && item.ContainedItems.Any())
{
logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")";
}
logStr += " on " + targetCharacter.LogName + ".";
Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack);
}
#endif
}
#endif
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
{
ApplyStatusEffects(success ? ActionType.OnUse : ActionType.OnFailure, 1.0f, targetCharacter, targetLimb, user: User, afflictionMultiplier: damageMultiplier);
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier);
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier);
}
if (DeleteOnUse)
@@ -23,6 +23,8 @@ namespace Barotrauma.Items.Components
[Serialize(0.0f, IsPropertySaveable.No, description: "The force to apply to the user's body."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float Force { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "If the item is held in RightHand or LeftHand, apply extra force there")]
public bool ApplyToHands { get; set; }
#if CLIENT
private string particles;
[Serialize("", IsPropertySaveable.No, description: "The name of the particle prefab the item emits when used.")]
@@ -70,13 +72,16 @@ namespace Barotrauma.Items.Components
character.AnimController.Collider.ApplyForce(propulsion);
if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand))
{
character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion);
}
if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
{
character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion);
if (ApplyToHands)
{
if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand))
{
character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion);
}
if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
{
character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion);
}
}
#if CLIENT
@@ -32,6 +32,20 @@ namespace Barotrauma.Items.Components
set { reload = Math.Max(value, 0.0f); }
}
[Serialize(0f, IsPropertySaveable.No, description: "Weapons skill requirement to reload at normal speed.")]
public float ReloadSkillRequirement
{
get;
set;
}
[Serialize(1.0f, IsPropertySaveable.No, description: "Reload time at 0 skill level. Reload time scales with skill level up to the Weapons skill requirement.")]
public float ReloadNoSkill
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Tells the AI to hold the trigger down when it uses this weapon")]
public bool HoldTrigger
{
@@ -39,7 +53,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(1, IsPropertySaveable.No, description: "How projectiles the weapon launches when fired once.")]
[Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")]
public int ProjectileCount
{
get;
@@ -60,6 +74,23 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched). Sum of weapon + projectile.")]
public float LaunchImpulse
{
get;
set;
}
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs). Sum of weapon + projectile."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)]
public float Penetration { get; private set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "Weapon's damage modifier")]
public float WeaponDamageModifier
{
get;
private set;
}
[Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")]
public float MaxChargeTime
{
@@ -99,6 +130,12 @@ namespace Barotrauma.Items.Components
// TODO: should define this in xml if we have ranged weapons that don't require aim to use
item.RequireAimToUse = true;
characterUsable = true;
if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload)
{
DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement.");
}
InitProjSpecific(element);
}
@@ -167,7 +204,15 @@ namespace Barotrauma.Items.Components
if (currentChargeTime < MaxChargeTime) { return false; }
IsActive = true;
ReloadTimer = reload / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
float baseReloadTime = reload;
float weaponSkill = character.GetSkillLevel("weapons");
if (ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement)
{
//Examples, assuming 40 weapon skill required: 1 - 40/40 = 0 ... 1 - 0/40 = 1 ... 1 - 20 / 40 = 0.5
float reloadFailure = MathHelper.Clamp(1 - (weaponSkill / ReloadSkillRequirement), 0, 1);
baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure);
}
ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
currentChargeTime = 0f;
if (character != null)
@@ -218,9 +263,9 @@ namespace Barotrauma.Items.Components
{
lastProjectile?.Item.GetComponent<Rope>()?.Snap();
}
float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier);
float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier;
projectile.Launcher = item;
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier);
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse);
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
if (i == 0)
{
@@ -1,17 +1,26 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class Throwable : Holdable
{
private float throwPos;
private bool throwing, throwDone;
enum ThrowState
{
None,
Initiated,
Throwing
}
private const float ThrowAngleStart = -MathHelper.PiOver2, ThrowAngleEnd = MathHelper.PiOver2;
private float throwAngle = ThrowAngleStart;
private bool midAir;
private ThrowState throwState;
//continuous collision detection is used while the item is moving faster than this
const float ContinuousCollisionThreshold = 5.0f;
@@ -27,7 +36,6 @@ namespace Barotrauma.Items.Components
public Throwable(Item item, ContentXElement element)
: base(item, element)
{
//throwForce = ToolBox.GetAttributeFloat(element, "throwforce", 1.0f);
if (aimPos == Vector2.Zero)
{
aimPos = new Vector2(0.6f, 0.1f);
@@ -36,22 +44,21 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
return characterUsable || character == null; //We do the actual throwing in Aim because Use might be used by chems
//actual throwing logic is handled in Update
return characterUsable || character == null;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
{
if (!throwDone) return false; //This should only be triggered in update
throwDone = false;
return true;
//actual throwing logic is handled in Update - SecondaryUse only triggers when the item is thrown
return false;
}
public override void Drop(Character dropper)
{
base.Drop(dropper);
throwing = false;
throwPos = 0.0f;
throwState = ThrowState.None;
throwAngle = ThrowAngleStart;
}
public override void UpdateBroken(float deltaTime, Camera cam)
@@ -100,13 +107,22 @@ namespace Barotrauma.Items.Components
return;
}
if (picker.IsKeyDown(InputType.Aim) && picker.IsKeyHit(InputType.Shoot)) { throwing = true; }
if (!picker.IsKeyDown(InputType.Aim) && !throwing) { throwPos = 0.0f; }
bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim;
if (throwState != ThrowState.Throwing)
{
if (picker.IsKeyDown(InputType.Aim))
{
if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; }
}
else if (throwState != ThrowState.Initiated)
{
throwAngle = ThrowAngleStart;
}
}
bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim;
if (picker.IsDead || !picker.AllowInput)
{
throwing = false;
throwState = ThrowState.None;
aim = false;
}
@@ -124,25 +140,29 @@ namespace Barotrauma.Items.Components
item.Submarine = picker.Submarine;
if (!throwing)
if (throwState != ThrowState.Throwing)
{
if (aim)
if (aim || throwState == ThrowState.Initiated)
{
throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos);
throwAngle = System.Math.Min(throwAngle + deltaTime * 8.0f, ThrowAngleEnd);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle);
if (throwAngle >= ThrowAngleEnd && throwState == ThrowState.Initiated)
{
throwState = ThrowState.Throwing;
}
}
else
{
throwPos = 0;
throwAngle = ThrowAngleStart;
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
}
}
else
{
throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos);
throwAngle = MathUtils.WrapAnglePi(throwAngle - deltaTime * 15.0f);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle);
if (throwPos < 0)
if (throwAngle < 0)
{
Vector2 throwVector = Vector2.Normalize(picker.CursorWorldPosition - picker.WorldPosition);
//throw upwards if cursor is at the position of the character
@@ -180,8 +200,7 @@ namespace Barotrauma.Items.Components
Limb rightHand = ac.GetLimb(LimbType.RightHand);
item.body.AngularVelocity = rightHand.body.AngularVelocity;
throwPos = 0;
throwDone = true;
throwAngle = ThrowAngleStart;
IsActive = true;
if (GameMain.NetworkMember is { IsServer: true })
@@ -193,7 +212,7 @@ namespace Barotrauma.Items.Components
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower);
}
throwing = false;
throwState = ThrowState.None;
}
}
}
@@ -65,8 +65,16 @@ namespace Barotrauma.Items.Components
public int Capacity
{
get { return capacity; }
set { capacity = Math.Max(value, 0); }
private set
{
capacity = Math.Max(value, 0);
MainContainerCapacity = value;
}
}
/// <summary>
/// The capacity of the main container without taking the sub containers into account. Only differs when there's a sub container defined for the component.
/// </summary>
public int MainContainerCapacity { get; private set; }
//how many items can be contained
private int maxStackSize;
@@ -229,6 +237,9 @@ namespace Barotrauma.Items.Components
public ImmutableHashSet<Identifier> ContainableItemIdentifiers => containableItemIdentifiers;
public List<RelatedItem> ContainableItems { get; }
public List<RelatedItem> AllSubContainableItems { get; }
public readonly bool HasSubContainers;
public ItemContainer(Item item, ContentXElement element)
: base(item, element)
@@ -251,6 +262,7 @@ namespace Barotrauma.Items.Components
break;
case "subcontainer":
totalCapacity += subElement.GetAttributeInt("capacity", 1);
HasSubContainers = true;
break;
}
}
@@ -270,7 +282,7 @@ namespace Barotrauma.Items.Components
int subCapacity = subElement.GetAttributeInt("capacity", 1);
int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize);
List<RelatedItem> subContainableItems = null;
var subContainableItems = new List<RelatedItem>();
foreach (var subSubElement in subElement.Elements())
{
if (subSubElement.Name.ToString().ToLowerInvariant() != "containable") { continue; }
@@ -281,8 +293,9 @@ namespace Barotrauma.Items.Components
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.");
continue;
}
subContainableItems ??= new List<RelatedItem>();
subContainableItems.Add(containable);
AllSubContainableItems ??= new List<RelatedItem>();
AllSubContainableItems.Add(containable);
}
for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++)
@@ -357,6 +370,14 @@ namespace Barotrauma.Items.Components
//no need to Update() if this item has no statuseffects and no physics body
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
if (IsActive && item.GetRootInventoryOwner() is Character owner &&
owner.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand)))
{
// Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it).
SetContainedActive(true);
}
OnContainedItemsChanged.Invoke(this);
}
@@ -409,6 +430,20 @@ namespace Barotrauma.Items.Components
return false;
}
public override void FlipX(bool relativeToSub)
{
base.FlipX(relativeToSub);
if (HideItems) { return; }
if (item.body == null) { return; }
foreach (Item containedItem in Inventory.AllItems)
{
if (containedItem.body != null && containedItem.body.Enabled && containedItem.body.Dir != item.body.Dir)
{
containedItem.FlipX(relativeToSub);
}
}
}
public override void Update(float deltaTime, Camera cam)
{
if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned)
@@ -477,7 +512,7 @@ namespace Barotrauma.Items.Components
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(item.WorldPosition, targets));
effect.AddNearbyTargets(item.WorldPosition, targets);
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
}
}
@@ -582,11 +617,53 @@ namespace Barotrauma.Items.Components
public override void Drop(Character dropper)
{
IsActive = true;
SetContainedActive(false);
}
public override void Equip(Character character)
{
IsActive = true;
if (character != null && character.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand)))
{
SetContainedActive(true);
}
}
private void SetContainedActive(bool active)
{
foreach (Item containedItem in Inventory.AllItems)
{
RelatedItem containableItem = FindContainableItem(containedItem);
if (containableItem != null && containableItem.SetActive)
{
foreach (var ic in containedItem.Components)
{
ic.IsActive = active;
}
if (containedItem.body != null)
{
containedItem.body.Enabled = active;
if (active)
{
containedItem.body.PhysEnabled = false;
}
}
}
}
if (active)
{
FlipX(false);
}
}
private RelatedItem FindContainableItem(Item item)
{
var relatedItem = ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item));
if (relatedItem == null && AllSubContainableItems != null)
{
relatedItem = AllSubContainableItems.FirstOrDefault(ci => ci.MatchesItem(item));
}
return relatedItem;
}
public override void ReceiveSignal(Signal signal, Connection connection)
@@ -604,6 +681,7 @@ namespace Barotrauma.Items.Components
}
}
#warning There's some code duplication here and in DrawContainedItems() method, but it's not straightforward to get rid of it, because of slightly different logic and the usage of draw positions vs. positions etc. Should probably be splitted into smaller methods.
public void SetContainedItemPositions()
{
Vector2 transformedItemPos = ItemPos * item.Scale;
@@ -657,29 +735,70 @@ namespace Barotrauma.Items.Components
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemPos += item.Position;
}
}
float currentRotation = itemRotation;
if (item.body != null)
{
currentRotation *= item.body.Dir;
currentRotation += item.body.Rotation;
}
else
{
currentRotation += -item.RotationRad;
}
int i = 0;
Vector2 currentItemPos = transformedItemPos;
foreach (Item contained in Inventory.AllItems)
{
Vector2 itemPos = currentItemPos;
var relatedItem = FindContainableItem(contained);
if (relatedItem != null)
{
if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; }
if (relatedItem.ItemPos.HasValue)
{
Vector2 pos = relatedItem.ItemPos.Value;
if (item.body != null)
{
Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
pos.X *= item.body.Dir;
itemPos = Vector2.Transform(pos, transform) + item.body.Position;
}
else
{
itemPos = pos;
// This code is aped based on above. Not tested.
if (item.FlippedX)
{
itemPos.X = -itemPos.X;
itemPos.X += item.Rect.Width;
}
if (item.FlippedY)
{
itemPos.Y = -itemPos.Y;
itemPos.Y -= item.Rect.Height;
}
itemPos += new Vector2(item.Rect.X, item.Rect.Y);
if (Math.Abs(item.RotationRad) > 0.01f)
{
Matrix transform = Matrix.CreateRotationZ(item.RotationRad);
itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position;
}
}
}
}
if (contained.body != null)
{
try
{
Vector2 simPos = ConvertUnits.ToSimUnits(currentItemPos);
contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, currentRotation);
Vector2 simPos = ConvertUnits.ToSimUnits(itemPos);
float rotation = itemRotation;
if (relatedItem != null && relatedItem.Rotation != 0)
{
rotation = MathHelper.ToRadians(relatedItem.Rotation);
}
if (item.body != null)
{
rotation *= item.body.Dir;
rotation += item.body.Rotation;
}
else
{
rotation += -item.RotationRad;
}
contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation);
contained.body.UpdateDrawPosition();
}
@@ -695,8 +814,8 @@ namespace Barotrauma.Items.Components
contained.Rect =
new Rectangle(
(int)(currentItemPos.X - contained.Rect.Width / 2.0f),
(int)(currentItemPos.Y + contained.Rect.Height / 2.0f),
(int)(itemPos.X - contained.Rect.Width / 2.0f),
(int)(itemPos.Y + contained.Rect.Height / 2.0f),
contained.Rect.Width, contained.Rect.Height);
contained.Submarine = item.Submarine;
@@ -459,6 +459,12 @@ namespace Barotrauma.Items.Components
progressTimer = 0.0f;
progressState = 0.0f;
}
#if CLIENT
else
{
HintManager.OnStartDeconstructing(user, this);
}
#endif
inputContainer.Inventory.Locked = IsActive;
}
@@ -419,7 +419,7 @@ namespace Barotrauma.Items.Components
}
var fabricationIngredients = new AbilityFabricationItemIngredients(foundAvailableItems);
user.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients);
user?.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients);
foreach (Item availableItem in fabricationIngredients.Items)
{
@@ -559,7 +559,7 @@ namespace Barotrauma.Items.Components
if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; }
int quality = 0;
float floatQuality = 0.0f;
floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality);
floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false);
foreach (var tag in fabricatedItem.TargetItem.Tags)
{
floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag);
@@ -740,10 +740,27 @@ namespace Barotrauma.Items.Components
//order by condition (prefer using worst-condition items)
int index = 0;
while (index < availableIngredients[itemIdentifier].Count &&
availableIngredients[itemIdentifier][index].Condition < item.Condition)
compare(item, availableIngredients[itemIdentifier][index], inputContainer.Inventory) < 0)
{
index++;
}
static int compare(Item item1, Item item2, Inventory inputInventory)
{
bool item1InInputInventory = item1.ParentInventory == inputInventory;
bool item2InInputInventory = item2.ParentInventory == inputInventory;
//prefer items in the input inventory
if (item1InInputInventory != item2InInputInventory)
{
return item1InInputInventory ? 1 : -1;
}
else
{
//prefer items in worse condition
return Math.Sign(item2.Condition - item1.Condition);
}
}
availableIngredients[itemIdentifier].Insert(index, item);
}
}
@@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components
public float? ReceivedOxygenAmount,
ReceivedWaterAmount;
public double LastOxygenDataTime, LastWaterDataTime;
public readonly HashSet<IdCard> Cards = new HashSet<IdCard>();
public bool Distort;
@@ -83,7 +85,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
//periodically reset all hull data
//reset data if we haven't received anything in a while
//(so that outdated hull info won't be shown if detectors stop sending signals)
if (DateTime.Now > resetDataTime)
{
@@ -91,8 +93,8 @@ namespace Barotrauma.Items.Components
{
if (!hullData.Distort)
{
hullData.ReceivedOxygenAmount = null;
hullData.ReceivedWaterAmount = null;
if (Timing.TotalTime > hullData.LastOxygenDataTime + 1.0) { hullData.ReceivedOxygenAmount = null; }
if (Timing.TotalTime > hullData.LastWaterDataTime + 1.0) { hullData.ReceivedWaterAmount = null; }
}
}
resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1);
@@ -159,6 +161,7 @@ namespace Barotrauma.Items.Components
//cheating a bit because water detectors don't actually send the water level
bool fromWaterDetector = source.GetComponent<WaterDetector>() != null;
hullData.ReceivedWaterAmount = null;
hullData.LastWaterDataTime = Timing.TotalTime;
if (fromWaterDetector)
{
hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull);
@@ -184,9 +187,10 @@ namespace Barotrauma.Items.Components
oxy = Rand.Range(0.0f, 100.0f);
}
hullData.ReceivedOxygenAmount = oxy;
hullData.LastOxygenDataTime = Timing.TotalTime;
foreach (var linked in sourceHull.linkedTo)
{
if (!(linked is Hull linkedHull)) { continue; }
if (linked is not Hull linkedHull) { continue; }
if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData))
{
linkedHullData = new HullData();
@@ -197,11 +197,6 @@ namespace Barotrauma.Items.Components
if (currentPingIndex != -1)
{
var activePing = activePings[currentPingIndex];
if (item.AiTarget != null)
{
float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom);
item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range);
}
if (activePing.State > 1.0f)
{
aiPingCheckPending = true;
@@ -235,6 +230,11 @@ namespace Barotrauma.Items.Components
for (var pingIndex = 0; pingIndex < activePingsCount;)
{
if (item.AiTarget != null)
{
float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom);
item.AiTarget.SoundRange = Math.Max(item.AiTarget.SoundRange, MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range));
}
if (activePings[pingIndex].State > 1.0f)
{
var lastIndex = --activePingsCount;
@@ -144,6 +144,11 @@ namespace Barotrauma.Items.Components
}
}
public float TargetVelocityLengthSquared
{
get => TargetVelocity.LengthSquared();
}
public Vector2 SteeringInput
{
get { return steeringInput; }
@@ -392,6 +392,7 @@ namespace Barotrauma.Items.Components
}
}
User = character;
ApplyStatusEffects(ActionType.OnUse, 1.0f, User, user: User);
return true;
}
@@ -916,23 +917,22 @@ namespace Barotrauma.Items.Components
if (character != null) { character.LastDamageSource = item; }
ActionType actionType = ActionType.OnUse;
if (_user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(_user))
ActionType conditionalActionType = ActionType.OnSuccess;
if (User != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(User))
{
actionType = ActionType.OnFailure;
conditionalActionType = ActionType.OnFailure;
}
#if CLIENT
PlaySound(actionType, user: _user);
PlaySound(ActionType.OnImpact, user: _user);
PlaySound(conditionalActionType, user: User);
PlaySound(ActionType.OnImpact, user: User);
#endif
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (target.Body.UserData is Limb targetLimb)
{
ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: _user);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: _user);
ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: User);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: User);
var attack = targetLimb.attack;
if (attack != null)
{
@@ -941,8 +941,6 @@ namespace Barotrauma.Items.Components
{
if (effect.type == ActionType.OnImpact)
{
//effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
@@ -951,32 +949,27 @@ namespace Barotrauma.Items.Components
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(targetLimb.WorldPosition, targets));
effect.AddNearbyTargets(targetLimb.WorldPosition, targets);
effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets);
}
}
}
}
#if SERVER
if (GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember is { IsServer: true } server)
{
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, targetLimb.character, targetLimb, null, item.WorldPosition));
GameMain.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, null, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition));
}
#endif
}
else
{
ApplyStatusEffects(actionType, 1.0f, useTarget: target.Body.UserData as Entity, user: _user);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: _user);
#if SERVER
if (GameMain.NetworkMember.IsServer)
ApplyStatusEffects(conditionalActionType, 1.0f, useTarget: target.Body.UserData as Entity, user: User);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User);
if (GameMain.NetworkMember is { IsServer: true } server)
{
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
GameMain.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, 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));
}
#endif
}
}
@@ -289,7 +289,7 @@ namespace Barotrauma.Items.Components
#if CLIENT
Light.ParentSub = item.Submarine;
#endif
if (item.Container != null)
if (item.Container != null && !(item.GetRootInventoryOwner() is Character))
{
SetLightSourceState(false, 0.0f);
return;
@@ -301,7 +301,7 @@ namespace Barotrauma.Items.Components
if (body != null && !body.Enabled)
{
SetLightSourceState(false, 0.0f);
return;
return;
}
//currPowerConsumption = powerConsumption;
@@ -138,7 +138,7 @@ namespace Barotrauma
private ConcurrentQueue<float> impactQueue;
//a dictionary containing lists of the status effects in all the components of the item
private readonly bool[] hasStatusEffectsOfType;
private readonly bool[] hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length];
private readonly Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; protected set; }
@@ -630,7 +630,7 @@ namespace Barotrauma
{
if (!spawnedInCurrentOutpost && value)
{
OriginalOutpost = GameMain.GameSession?.StartLocation?.BaseName ?? "";
OriginalOutpost = GameMain.GameSession?.LevelData?.Seed;
}
spawnedInCurrentOutpost = value;
}
@@ -651,7 +651,9 @@ namespace Barotrauma
set
{
originalOutpost = value;
if (!string.IsNullOrEmpty(value) && GameMain.GameSession?.LevelData?.Type == LevelData.LevelType.Outpost && GameMain.GameSession?.StartLocation?.BaseName == value)
if (!string.IsNullOrEmpty(value) &&
GameMain.GameSession?.LevelData?.Type == LevelData.LevelType.Outpost &&
GameMain.GameSession?.LevelData?.Seed == value)
{
spawnedInCurrentOutpost = true;
}
@@ -1005,7 +1007,6 @@ namespace Barotrauma
}
}
hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length];
foreach (ItemComponent ic in components)
{
if (ic is Pickable pickable)
@@ -1654,7 +1655,7 @@ namespace Barotrauma
if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters) || effect.HasTargetType(StatusEffect.TargetType.NearbyItems))
{
targets.AddRange(effect.GetNearbyTargets(WorldPosition, targets));
effect.AddNearbyTargets(WorldPosition, targets);
if (targets.Count > 0)
{
hasTargets = true;
@@ -2764,18 +2765,20 @@ namespace Barotrauma
if (!ic.HasRequiredContainedItems(user, addMessage: user == Character.Controlled)) { continue; }
bool success = Rand.Range(0.0f, 0.5f) < ic.DegreeOfSuccess(user);
ActionType actionType = success ? ActionType.OnUse : ActionType.OnFailure;
ActionType conditionalActionType = success ? ActionType.OnSuccess : ActionType.OnFailure;
#if CLIENT
ic.PlaySound(actionType, user);
ic.PlaySound(conditionalActionType, user);
ic.PlaySound(ActionType.OnUse, user);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user);
ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: user);
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user);
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(
actionType, ic, character, targetLimb));
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb));
}
if (ic.DeleteOnUse) { remove = true; }
@@ -814,8 +814,6 @@ namespace Barotrauma
? category
: MapEntityCategory.Misc;
var parentType = ConfigElement.Parent?.GetAttributeIdentifier("itemtype", "");
//nameidentifier can be used to make multiple items use the same names and descriptions
Identifier nameIdentifier = ConfigElement.GetAttributeIdentifier("nameidentifier", "");
@@ -831,7 +829,7 @@ namespace Barotrauma
name = name.Fallback(OriginalName);
}
if (parentType == "wrecked")
if (category == MapEntityCategory.Wrecked)
{
name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name);
}
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
@@ -57,6 +58,20 @@ namespace Barotrauma
/// </summary>
public int TargetSlot = -1;
/// <summary>
/// Overrides the position defined in ItemContainer.
/// </summary>
public Vector2? ItemPos;
/// <summary>
/// Only affects when ItemContainer.hideItems is false. Doesn't override the value.
/// </summary>
public bool? Hide;
public float Rotation;
public bool SetActive;
public string JoinedIdentifiers
{
get { return string.Join(",", Identifiers); }
@@ -202,7 +217,18 @@ namespace Barotrauma
new XAttribute("requireempty", RequireEmpty),
new XAttribute("excludefullcondition", ExcludeFullCondition),
new XAttribute("targetslot", TargetSlot),
new XAttribute("allowvariants", AllowVariants));
new XAttribute("allowvariants", AllowVariants),
new XAttribute("rotation", Rotation),
new XAttribute("setactive", SetActive));
if (Hide.HasValue)
{
element.Add(new XAttribute(nameof(Hide), Hide.Value));
}
if (ItemPos.HasValue)
{
element.Add(new XAttribute(nameof(ItemPos), ItemPos.Value));
}
if (excludedIdentifiers.Count > 0)
{
@@ -267,8 +293,18 @@ namespace Barotrauma
ExcludeBroken = element.GetAttributeBool("excludebroken", true),
RequireEmpty = element.GetAttributeBool("requireempty", false),
ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false),
AllowVariants = element.GetAttributeBool("allowvariants", true)
AllowVariants = element.GetAttributeBool("allowvariants", true),
Rotation = element.GetAttributeFloat("rotation", 0f),
SetActive = element.GetAttributeBool("setactive", false)
};
if (element.GetAttribute(nameof(Hide)) != null)
{
ri.Hide = element.GetAttributeBool(nameof(Hide), false);
}
if (element.GetAttribute(nameof(ItemPos)) != null)
{
ri.ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero);
}
string typeStr = element.GetAttributeString("type", "");
if (string.IsNullOrEmpty(typeStr))
{
@@ -1,3 +1,5 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma
@@ -18,6 +20,14 @@ namespace Barotrauma
public readonly ImmutableHashSet<int> AllowedZones;
private readonly SubmarineAvailability? submarineAvailability;
private readonly ImmutableHashSet<SubmarineAvailability> submarineAvailabilityOverrides;
public readonly record struct SubmarineAvailability(
Identifier LocationType,
SubmarineClass Class = SubmarineClass.Undefined,
int MaxTier = 0);
public Biome(ContentXElement element, LevelGenerationParametersFile file) : base(file, ParseIdentifier(element))
{
OldIdentifier = element.GetAttributeIdentifier("oldidentifier", Identifier.Empty);
@@ -34,6 +44,26 @@ namespace Barotrauma
AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet();
MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0);
maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100);
var submarineAvailabilityOverrides = new HashSet<SubmarineAvailability>();
if (element.GetChildElement("submarines") is ContentXElement availabilityElement)
{
submarineAvailability = GetAvailability(availabilityElement);
foreach (var overrideElement in availabilityElement.GetChildElements("override"))
{
var availabilityOverride = GetAvailability(overrideElement);
submarineAvailabilityOverrides.Add(availabilityOverride);
}
}
this.submarineAvailabilityOverrides = submarineAvailabilityOverrides.ToImmutableHashSet();
static SubmarineAvailability GetAvailability(ContentXElement element)
{
return new SubmarineAvailability(
LocationType: element.GetAttributeIdentifier("locationtype", Identifier.Empty),
Class: element.GetAttributeEnum("class", SubmarineClass.Undefined),
MaxTier: element.GetAttributeInt("maxtier", 0));
}
}
public static Identifier ParseIdentifier(ContentXElement element)
@@ -47,6 +77,31 @@ namespace Barotrauma
return identifier;
}
public int HighestSubmarineTierAvailable(SubmarineClass subClass, Identifier locationType)
{
if (!submarineAvailability.HasValue)
{
// If the availability is not explicitly defined, make all subs available
return SubmarineInfo.HighestTier;
}
int maxTier = submarineAvailability.Value.MaxTier;
if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == locationType && a.Class == subClass) is SubmarineAvailability locationAndClassOverride)
{
maxTier = locationAndClassOverride.MaxTier;
}
else if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == locationType && a.Class == SubmarineClass.Undefined) is SubmarineAvailability locationOverride)
{
maxTier = locationOverride.MaxTier;
}
else if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == Identifier.Empty && a.Class == subClass) is SubmarineAvailability classOverride)
{
maxTier = classOverride.MaxTier;
}
return maxTier;
}
public bool IsSubmarineAvailable(SubmarineInfo info, Identifier locationType) => info.Tier <= HighestSubmarineTierAvailable(info.SubmarineClass, locationType);
public override void Dispose() { }
}
}
@@ -447,7 +447,7 @@ namespace Barotrauma
private Level(LevelData levelData) : base(null, 0)
{
this.LevelData = levelData;
LevelData = levelData;
borders = new Rectangle(Point.Zero, levelData.Size);
}
@@ -3939,7 +3939,7 @@ namespace Barotrauma
}
SubmarineInfo outpostInfo;
Submarine outpost;
Submarine outpost = null;
if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null)
{
if (OutpostGenerationParams.OutpostParams.Any() || LevelData.ForceOutpostGenerationParams != null)
@@ -59,6 +59,7 @@ namespace Barotrauma
public readonly List<EventPrefab> EventHistory = new List<EventPrefab>();
public readonly List<EventPrefab> NonRepeatableEvents = new List<EventPrefab>();
public readonly HashSet<Identifier> UsedUniqueSets = new HashSet<Identifier>();
public bool EventsExhausted { get; set; }
@@ -143,10 +144,11 @@ namespace Barotrauma
string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { });
NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)));
UsedUniqueSets = element.GetAttributeIdentifierArray(nameof(UsedUniqueSets), Array.Empty<Identifier>()).ToHashSet();
EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false);
}
/// <summary>
/// Instantiates level data using the properties of the connection (seed, size, difficulty)
/// </summary>
@@ -284,6 +286,12 @@ namespace Barotrauma
newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents.Select(p => p.Identifier))));
}
}
if (UsedUniqueSets.Any())
{
newElement.Add(new XAttribute(nameof(UsedUniqueSets), string.Join(',', UsedUniqueSets)));
}
parentElement.Add(newElement);
}
}
@@ -287,6 +287,13 @@ namespace Barotrauma
private set;
}
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable]
public Color SpriteColor
{
get;
private set;
}
public string Name => Identifier.Value;
public List<ChildObject> ChildObjects
@@ -661,7 +661,7 @@ namespace Barotrauma
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
effect.AddNearbyTargets(worldPosition, targets);
effect.Apply(effect.type, deltaTime, triggerer, targets);
}
}
@@ -61,7 +61,7 @@ namespace Barotrauma
private LocationType addInitialMissionsForType;
public bool Discovered { get; private set; }
public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false;
public readonly Dictionary<LocationTypeChange.Requirement, int> ProximityTimer = new Dictionary<LocationTypeChange.Requirement, int>();
public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange;
@@ -287,12 +287,12 @@ namespace Barotrauma
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (characters.Any())
{
if (Location.Reputation?.Faction is { } faction && faction.IsAffiliated())
if (Location.Reputation?.Faction is { } faction && faction.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated)
{
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated));
}
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier));
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag)));
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false));
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag)));
}
// Price should never go below 1 mk
return Math.Max((int)price, 1);
@@ -497,7 +497,6 @@ namespace Barotrauma
baseName = element.GetAttributeString("basename", "");
Name = element.GetAttributeString("name", "");
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
Discovered = element.GetAttributeBool("discovered", false);
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
@@ -1292,14 +1291,17 @@ namespace Barotrauma
return characters.Sum(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount));
}
public void Discover(bool checkTalents = true)
public int HighestSubmarineTierAvailable(SubmarineClass submarineClass)
{
if (Discovered) { return; }
Discovered = true;
if (checkTalents)
{
GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new AbilityLocation(this)));
}
if (!HasOutpost()) { return 0; }
return Biome?.HighestSubmarineTierAvailable(submarineClass, Type.Identifier) ?? SubmarineInfo.HighestTier;
}
public int HighestSubmarineTierAvailable() => HighestSubmarineTierAvailable(SubmarineClass.Undefined);
public bool IsSubmarineAvailable(SubmarineInfo info)
{
return Biome?.IsSubmarineAvailable(info, Type.Identifier) ?? true;
}
public void Reset()
@@ -1313,7 +1315,6 @@ namespace Barotrauma
ClearMissions();
LevelData?.EventHistory?.Clear();
UnlockInitialMissions();
Discovered = false;
}
public XElement Save(Map map, XElement parentElement)
@@ -1324,7 +1325,6 @@ namespace Barotrauma
new XAttribute("basename", BaseName),
new XAttribute("name", Name),
new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty),
new XAttribute("discovered", Discovered),
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
new XAttribute("pricemultiplier", PriceMultiplier),
new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes),
@@ -1453,7 +1453,7 @@ namespace Barotrauma
HireManager?.Remove();
}
class AbilityLocation : AbilityObject, IAbilityLocation
public class AbilityLocation : AbilityObject, IAbilityLocation
{
public AbilityLocation(Location location)
{
@@ -24,6 +24,7 @@ namespace Barotrauma
public readonly Dictionary<int, int> MinCountPerZone = new Dictionary<int, int>();
public readonly LocalizedString Name;
public readonly LocalizedString Description;
public readonly float BeaconStationChance;
@@ -70,6 +71,13 @@ namespace Barotrauma
public Sprite Sprite { get; private set; }
public Sprite RadiationSprite { get; }
private readonly Identifier forceOutpostGenerationParamsIdentifier;
/// <summary>
/// If set to true, only event sets that explicitly define this location type in <see cref="EventSet.LocationTypeIdentifiers"/> can be selected at this location. Defaults to false.
/// </summary>
public bool IgnoreGenericEvents { get; }
public Color SpriteColor
{
get;
@@ -96,6 +104,7 @@ namespace Barotrauma
public LocationType(ContentXElement element, LocationTypesFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName))
{
Name = TextManager.Get("LocationName." + Identifier, "unknown");
Description = TextManager.Get("LocationDescription." + Identifier, "");
BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f);
@@ -110,6 +119,10 @@ namespace Barotrauma
ReplaceInRadiation = element.GetAttributeIdentifier(nameof(ReplaceInRadiation), Identifier.Empty);
forceOutpostGenerationParamsIdentifier = element.GetAttributeIdentifier("forceoutpostgenerationparams", Identifier.Empty);
IgnoreGenericEvents = element.GetAttributeBool(nameof(IgnoreGenericEvents), false);
string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC");
Enum.TryParse(teamStr, out OutpostTeam);
@@ -261,6 +274,15 @@ namespace Barotrauma
}
}
public OutpostGenerationParams GetForcedOutpostGenerationParams()
{
if (OutpostGenerationParams.OutpostParams.TryGet(forceOutpostGenerationParamsIdentifier, out var parameters))
{
return parameters;
}
return null;
}
public override void Dispose() { }
}
}
@@ -68,10 +68,15 @@ namespace Barotrauma
public List<Location> Locations { get; private set; }
private readonly List<Location> locationsDiscovered = new List<Location>();
private readonly List<Location> outpostsVisited = new List<Location>();
public List<LocationConnection> Connections { get; private set; }
public Radiation Radiation;
private bool wasLocationDiscoveryOrderTracked = true;
public Map(CampaignSettings settings)
{
generationParams = MapGenerationParams.Instance;
@@ -282,7 +287,12 @@ namespace Barotrauma
}
}
CurrentLocation.Discover(true);
if (campaign.IsSinglePlayer && campaign.Settings.TutorialEnabled && LocationType.Prefabs.TryGet("tutorialoutpost", out var tutorialOutpost))
{
CurrentLocation.ChangeType(tutorialOutpost);
}
Discover(CurrentLocation);
Visit(CurrentLocation);
CurrentLocation.CreateStores();
foreach (var location in Locations)
@@ -820,7 +830,8 @@ namespace Barotrauma
SelectedConnection.Passed = true;
CurrentLocation = SelectedLocation;
CurrentLocation.Discover();
Discover(CurrentLocation);
Visit(CurrentLocation);
SelectedLocation = null;
CurrentLocation.CreateStores();
@@ -851,7 +862,7 @@ namespace Barotrauma
Location prevLocation = CurrentLocation;
CurrentLocation = Locations[index];
CurrentLocation.Discover();
Discover(CurrentLocation);
CurrentLocation.CreateStores();
if (prevLocation != CurrentLocation)
@@ -1184,6 +1195,51 @@ namespace Barotrauma
partial void ClearAnimQueue();
public void Discover(Location location, bool checkTalents = true)
{
if (location is null) { return; }
if (locationsDiscovered.Contains(location)) { return; }
locationsDiscovered.Add(location);
if (checkTalents)
{
GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new Location.AbilityLocation(location)));
}
}
public void Visit(Location location)
{
if (location is null) { return; }
if (!location.HasOutpost()) { return; }
if (outpostsVisited.Contains(location)) { return; }
outpostsVisited.Add(location);
}
public void ClearLocationHistory()
{
locationsDiscovered.Clear();
outpostsVisited.Clear();
}
public int? GetDiscoveryIndex(Location location)
{
if (!wasLocationDiscoveryOrderTracked) { return null; }
if (location is null) { return -1; }
return locationsDiscovered.IndexOf(location);
}
public int? GetVisitIndex(Location location)
{
if (!wasLocationDiscoveryOrderTracked) { return null; }
if (location is null) { return -1; }
return outpostsVisited.IndexOf(location);
}
public bool IsDiscovered(Location location)
{
if (location is null) { return false; }
return locationsDiscovered.Contains(location);
}
/// <summary>
/// Load a previously saved map from an xml element
/// </summary>
@@ -1211,6 +1267,7 @@ namespace Barotrauma
return;
}
ClearLocationHistory();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -1226,19 +1283,12 @@ namespace Barotrauma
}
}
location.LoadLocationTypeChange(subElement);
// Backwards compatibility
if (subElement.GetAttributeBool("discovered", false))
{
location.Discover(checkTalents: false);
}
if (location.Discovered)
{
#if CLIENT
RemoveFogOfWar(location);
#endif
if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
{
furthestDiscoveredLocation = location;
}
Discover(location);
wasLocationDiscoveryOrderTracked = false;
}
Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
@@ -1268,6 +1318,36 @@ namespace Barotrauma
case "radiation":
Radiation = new Radiation(this, generationParams.RadiationParams, subElement);
break;
case "discovered":
foreach (var childElement in subElement.GetChildElements("location"))
{
int index = childElement.GetAttributeInt("i", -1);
if (index < 0) { continue; }
if (Locations[index] is not Location l) { continue; }
Discover(l);
}
break;
case "visited":
foreach (var childElement in subElement.GetChildElements("location"))
{
int index = childElement.GetAttributeInt("i", -1);
if (index < 0) { continue; }
if (Locations[index] is not Location l) { continue; }
Visit(l);
}
break;
}
}
void Discover(Location location)
{
this.Discover(location, checkTalents: false);
#if CLIENT
RemoveFogOfWar(location);
#endif
if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
{
furthestDiscoveredLocation = location;
}
}
@@ -1343,6 +1423,30 @@ namespace Barotrauma
mapElement.Add(Radiation.Save());
}
if (locationsDiscovered.Any())
{
var discoveryElement = new XElement("discovered");
foreach (Location location in locationsDiscovered)
{
int index = Locations.IndexOf(location);
var locationElement = new XElement("location", new XAttribute("i", index));
discoveryElement.Add(locationElement);
}
mapElement.Add(discoveryElement);
}
if (outpostsVisited.Any())
{
var visitElement = new XElement("visited");
foreach (Location location in outpostsVisited)
{
int index = Locations.IndexOf(location);
var locationElement = new XElement("location", new XAttribute("i", index));
visitElement.Add(locationElement);
}
mapElement.Add(visitElement);
}
element.Add(mapElement);
}
@@ -96,6 +96,8 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes), Editable]
public string ReplaceInRadiation { get; set; }
public ContentPath OutpostFilePath { get; set; }
public class ModuleCount
{
public Identifier Identifier;
@@ -182,6 +184,7 @@ namespace Barotrauma
Name = element.GetAttributeString("name", Identifier.Value);
allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty<Identifier>()).ToHashSet();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
OutpostFilePath = element.GetAttributeContentPath(nameof(OutpostFilePath));
var humanPrefabCollections = new List<IReadOnlyList<HumanPrefab>>();
foreach (var subElement in element.Elements())
@@ -143,7 +143,7 @@ namespace Barotrauma
//select which module types the outpost should consist of
List<Identifier> pendingModuleFlags =
onlyEntrance ?
generationParams.ModuleCounts.First().Identifier.ToEnumerable().ToList() :
(generationParams.ModuleCounts.FirstOrDefault()?.Identifier.ToEnumerable() ?? Enumerable.Empty<Identifier>()).ToList() :
SelectModules(outpostModules, generationParams);
foreach (Identifier flag in pendingModuleFlags)
@@ -246,6 +246,7 @@ namespace Barotrauma
var outpostFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostFile>())
.Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path))
.OrderBy(f => f.UintIdentifier).ToArray();
if (!outpostFiles.Any())
{
@@ -696,6 +697,14 @@ namespace Barotrauma
rect.Location += (module.Offset + module.MoveOffset).ToPoint();
rect.Y += module.Bounds.Height;
Vector2? selfGapPos1 = null;
Vector2? selfGapPos2 = null;
if (module.PreviousModule != null)
{
selfGapPos1 = module.Offset + module.ThisGap.Position + module.MoveOffset;
selfGapPos2 = module.PreviousModule.Offset + module.PreviousGap.Position + module.PreviousModule.MoveOffset;
}
foreach (PlacedModule otherModule in modules)
{
if (otherModule == module || otherModule.PreviousModule == null || otherModule.PreviousModule == module) { continue; }
@@ -710,7 +719,17 @@ namespace Barotrauma
Vector2 gapPos1 = otherModule.Offset + otherModule.ThisGap.Position + gapEdgeOffset + otherModule.MoveOffset;
Vector2 gapPos2 = otherModule.PreviousModule.Offset + otherModule.PreviousGap.Position + gapEdgeOffset + otherModule.PreviousModule.MoveOffset;
if (Submarine.RectContains(rect, gapPos1) || Submarine.RectContains(rect, gapPos2) || MathUtils.GetLineRectangleIntersection(gapPos1, gapPos2, rect, out _))
if (Submarine.RectContains(rect, gapPos1) ||
Submarine.RectContains(rect, gapPos2) ||
MathUtils.GetLineRectangleIntersection(gapPos1, gapPos2, rect, out _))
{
return true;
}
//check if the connection overlaps with this module's connection
if (selfGapPos1.HasValue && selfGapPos2.HasValue &&
!gapPos1.NearlyEquals(gapPos2) && !selfGapPos1.Value.NearlyEquals(selfGapPos2.Value) &&
MathUtils.LinesIntersect(gapPos1, gapPos2, selfGapPos1.Value, selfGapPos2.Value))
{
return true;
}
@@ -55,7 +55,7 @@ namespace Barotrauma
public PriceInfo(int price, bool canBeBought,
int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f,
bool displayNonEmpty = false, string storeIdentifier = null)
bool displayNonEmpty = false, bool requiresUnlock = false, string storeIdentifier = null)
{
Price = price;
CanBeBought = canBeBought;
@@ -67,6 +67,7 @@ namespace Barotrauma
CanBeSpecial = canBeSpecial;
DisplayNonEmpty = displayNonEmpty;
StoreIdentifier = new Identifier(storeIdentifier);
RequiresUnlock = requiresUnlock;
}
public static List<PriceInfo> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
@@ -81,6 +82,7 @@ namespace Barotrauma
float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false);
bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true));
bool requiresUnlock = element.GetAttributeBool("requiresunlock", false);
foreach (XElement childElement in element.GetChildElements("price"))
{
float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f);
@@ -94,26 +96,28 @@ namespace Barotrauma
}
string storeIdentifier = childElement.GetAttributeString("storeidentifier", backwardsCompatibleIdentifier);
// TODO: Add some error messages if we have defined the min or max amount while the item is not sold
var priceInfo = new PriceInfo((int)(priceMultiplier * basePrice),
sold,
sold ? GetMinAmount(childElement, minAmount) : 0,
sold ? GetMaxAmount(childElement, maxAmount) : 0,
canBeSpecial,
storeMinLevelDifficulty,
storeBuyingMultiplier,
displayNonEmpty,
storeIdentifier);
var priceInfo = new PriceInfo(price: (int)(priceMultiplier * basePrice),
canBeBought: sold,
minAmount: sold ? GetMinAmount(childElement, minAmount) : 0,
maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0,
canBeSpecial: canBeSpecial,
minLevelDifficulty: storeMinLevelDifficulty,
buyingPriceMultiplier: storeBuyingMultiplier,
displayNonEmpty: displayNonEmpty,
requiresUnlock: requiresUnlock,
storeIdentifier: storeIdentifier);
priceInfos.Add(priceInfo);
}
bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false));
defaultPrice = new PriceInfo(basePrice,
soldElsewhere,
soldElsewhere ? minAmount : 0,
soldElsewhere ? maxAmount : 0,
canBeSpecial,
minLevelDifficulty,
buyingPriceMultiplier,
displayNonEmpty);
defaultPrice = new PriceInfo(price: basePrice,
canBeBought: soldElsewhere,
minAmount: soldElsewhere ? minAmount : 0,
maxAmount: soldElsewhere ? maxAmount : 0,
canBeSpecial: canBeSpecial,
minLevelDifficulty: minLevelDifficulty,
buyingPriceMultiplier: buyingPriceMultiplier,
displayNonEmpty: displayNonEmpty,
requiresUnlock: requiresUnlock);
return priceInfos;
}
@@ -1114,7 +1114,8 @@ namespace Barotrauma
{
if (item.Submarine != this) { continue; }
var pump = item.GetComponent<Pump>();
if (pump == null || !item.HasTag("ballast") || item.CurrentHull == null) { continue; }
if (pump == null || item.CurrentHull == null) { continue; }
if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; }
pump.FlowPercentage = 0.0f;
ballastHulls.Add(item.CurrentHull);
}
@@ -314,6 +314,7 @@ namespace Barotrauma
Tier = original.Tier;
IsManuallyOutfitted = original.IsManuallyOutfitted;
Tags = original.Tags;
OutpostGenerationParams = original.OutpostGenerationParams;
if (original.OutpostModuleInfo != null)
{
OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo);
@@ -747,6 +748,8 @@ namespace Barotrauma
return doc;
}
public static int GetDefaultTier(int price) => price > 20000 ? 3 : price > 10000 ? 2 : 1;
public static int GetDefaultTier(int price) => price > 20000 ? HighestTier : price > 10000 ? 2 : 1;
public const int HighestTier = 3;
}
}
@@ -27,7 +27,8 @@ namespace Barotrauma.Networking
SellSubItems = 0x4000,
ManageMap = 0x8000,
ManageHires = 0x10000,
All = 0x1FFFF
ManageBotTalents = 0x20000,
All = 0x3FFFF
}
class PermissionPreset
@@ -382,12 +382,17 @@ namespace Barotrauma.Networking
private bool autoRestart;
public bool IsPublic;
private int maxPlayers;
public List<SavedClientPermission> ClientPermissions { get; private set; } = new List<SavedClientPermission>();
[Serialize(true, IsPropertySaveable.Yes)]
public bool IsPublic
{
get;
set;
}
private int tickRate = 20;
[Serialize(20, IsPropertySaveable.Yes)]
public int TickRate
@@ -518,13 +523,20 @@ namespace Barotrauma.Networking
}
}
[Serialize(Barotrauma.LosMode.Opaque, IsPropertySaveable.Yes)]
[Serialize(LosMode.Opaque, IsPropertySaveable.Yes)]
public LosMode LosMode
{
get;
set;
}
[Serialize(EnemyHealthBarMode.ShowAll, IsPropertySaveable.Yes)]
public EnemyHealthBarMode ShowEnemyHealthBars
{
get;
set;
}
[Serialize(800, IsPropertySaveable.Yes)]
public int LinesPerLogFile
{
@@ -35,10 +35,19 @@ namespace Barotrauma
Activity
}
public enum EnemyHealthBarMode
{
ShowAll,
BossHealthBarsOnly,
HideAll
}
public static class GameSettings
{
public struct Config
{
public const float DefaultAimAssist = 0.05f;
public static Config GetDefault()
{
Config config = new Config
@@ -50,7 +59,8 @@ namespace Barotrauma
SubEditorBackground = new Color(13, 37, 69, 255),
EnableSplashScreen = true,
PauseOnFocusLost = true,
AimAssistAmount = 0.5f,
AimAssistAmount = DefaultAimAssist,
ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll,
EnableMouseLook = true,
ChatOpen = true,
CrewMenuOpen = true,
@@ -110,6 +120,7 @@ namespace Barotrauma
public LanguageIdentifier Language;
public bool VerboseLogging;
public bool SaveDebugConsoleLogs;
public string SavePath;
public int SubEditorUndoBuffer;
public int MaxAutoSaves;
public int AutoSaveIntervalSeconds;
@@ -118,6 +129,7 @@ namespace Barotrauma
public bool PauseOnFocusLost;
public float AimAssistAmount;
public bool EnableMouseLook;
public EnemyHealthBarMode ShowEnemyHealthBars;
public bool ChatOpen;
public bool CrewMenuOpen;
public bool EditorDisclaimerShown;
@@ -24,7 +24,8 @@ namespace Barotrauma
HasSpecifierTag,
Affliction,
EntityType,
LimbType
LimbType,
SkillRequirement
}
public enum Comparison
@@ -73,6 +74,7 @@ namespace Barotrauma
case "targetcontainer":
case "targetgrandparent":
case "targetcontaineditem":
case "skillrequirement":
return false;
default:
return true;
@@ -110,6 +112,11 @@ namespace Barotrauma
{
Type = ConditionType.Uncertain;
}
if (attribute.Parent.GetAttributeBool("skillrequirement", false))
{
Type = ConditionType.SkillRequirement;
}
AttributeValue = valueString;
SplitAttributeValue = valueString.Split(',');
@@ -305,25 +312,20 @@ namespace Barotrauma
if (health == null) { return false; }
var affliction = health.GetAffliction(AttributeName.ToIdentifier());
float afflictionStrength = affliction == null ? 0.0f : affliction.Strength;
if (FloatValue.HasValue)
{
float value = FloatValue.Value;
switch (Operator)
{
case OperatorType.Equals:
return afflictionStrength == value;
case OperatorType.GreaterThan:
return afflictionStrength > value;
case OperatorType.GreaterThanEquals:
return afflictionStrength >= value;
case OperatorType.LessThan:
return afflictionStrength < value;
case OperatorType.LessThanEquals:
return afflictionStrength <= value;
case OperatorType.NotEquals:
return afflictionStrength != value;
}
}
return ValueMatchesRequirement(afflictionStrength);
}
}
return false;
case ConditionType.SkillRequirement:
{
if (target == null) { return Operator == OperatorType.NotEquals; }
if (target is Character targetChar)
{
float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier());
return ValueMatchesRequirement(skillLevel);
}
}
return false;
@@ -332,6 +334,30 @@ namespace Barotrauma
}
}
private bool ValueMatchesRequirement(float testedValue)
{
if (FloatValue.HasValue)
{
float value = FloatValue.Value;
switch (Operator)
{
case OperatorType.Equals:
return testedValue == value;
case OperatorType.GreaterThan:
return testedValue > value;
case OperatorType.GreaterThanEquals:
return testedValue >= value;
case OperatorType.LessThan:
return testedValue < value;
case OperatorType.LessThanEquals:
return testedValue <= value;
case OperatorType.NotEquals:
return testedValue != value;
}
}
return false;
}
private bool MatchesTagCondition(ISerializableEntity target)
{
if (!(target is Item item)) { return Operator == OperatorType.NotEquals; }
@@ -877,10 +877,9 @@ namespace Barotrauma
return true;
}
public IReadOnlyList<ISerializableEntity> GetNearbyTargets(Vector2 worldPosition, List<ISerializableEntity> targets = null)
public void AddNearbyTargets(Vector2 worldPosition, List<ISerializableEntity> targets)
{
targets ??= new List<ISerializableEntity>();
if (Range <= 0.0f) { return targets; }
if (Range <= 0.0f) { return; }
if (HasTargetType(TargetType.NearbyCharacters))
{
foreach (Character c in Character.CharacterList)
@@ -917,7 +916,6 @@ namespace Barotrauma
}
}
}
return targets;
bool CheckDistance(ISpatialEntity e)
{
@@ -1362,7 +1360,7 @@ namespace Barotrauma
{
if (breakLimb)
{
targetLimb.character.TrySeverLimbJoints(targetLimb, severLimbsProbability: 1, damage: -1, allowBeheading: true, attacker: user);
targetLimb.character.TrySeverLimbJoints(targetLimb, severLimbsProbability: 1, damage: -1, allowBeheading: true, ignoreSeveranceProbabilityModifier: true, attacker: user);
}
if (hideLimb)
{
@@ -2163,7 +2161,7 @@ namespace Barotrauma
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)
if (type == ActionType.OnUse || type == ActionType.OnSuccess)
{
limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
}
@@ -310,7 +310,7 @@ namespace Barotrauma.Steam
+ "unexpected deletion of your hard work.\n"
+ "Instead, modify a copy of your mod in LocalMods.\n";
string workshopModDirReadmeLocation = Path.Combine(SaveUtil.SaveFolder, "WorkshopMods", "README.txt");
string workshopModDirReadmeLocation = Path.Combine(SaveUtil.DefaultSaveFolder, "WorkshopMods", "README.txt");
if (!File.Exists(workshopModDirReadmeLocation))
{
Directory.CreateDirectory(Path.GetDirectoryName(workshopModDirReadmeLocation)!);
@@ -108,6 +108,8 @@ namespace Barotrauma
public readonly bool IsWallUpgrade;
public readonly LocalizedString Name;
private readonly object mutex = new object();
public readonly IEnumerable<Identifier> ItemTags;
public UpgradeCategory(ContentXElement element, UpgradeModulesFile file) : base(element, file)
@@ -119,7 +121,6 @@ namespace Barotrauma
ItemTags = selfItemTags.CollectionConcat(prefabsThatAllowUpgrades);
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
if (!nameIdentifier.IsEmpty)
{
Name = TextManager.Get($"{nameIdentifier}");
@@ -132,10 +133,13 @@ namespace Barotrauma
public void DeterminePrefabsThatAllowUpgrades()
{
prefabsThatAllowUpgrades.Clear();
prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs
.Where(it => it.GetAllowedUpgrades().Contains(Identifier))
.Select(it => it.Identifier));
lock (mutex)
{
prefabsThatAllowUpgrades.Clear();
prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs
.Where(it => it.GetAllowedUpgrades().Contains(Identifier))
.Select(it => it.Identifier));
}
}
public bool CanBeApplied(MapEntity item, UpgradePrefab? upgradePrefab)
@@ -153,26 +157,11 @@ namespace Barotrauma
if (upgradePrefab != null && upgradePrefab.IsDisallowed(item)) { return false; }
return item.Prefab.GetAllowedUpgrades().Contains(Identifier) ||
ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag);
}
public bool CanBeApplied(XElement element, UpgradePrefab prefab)
{
if ("Structure" == element.NameAsIdentifier()) { return IsWallUpgrade; }
Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier.IsEmpty) { return false; }
ItemPrefab? item = ItemPrefab.Find(null, identifier);
if (item == null) { return false; }
Identifier[] disallowedUpgrades = element.GetAttributeIdentifierArray("disallowedupgrades", Array.Empty<Identifier>());
if (disallowedUpgrades.Any(s => s == Identifier || s == prefab.Identifier)) { return false; }
return item.GetAllowedUpgrades().Contains(Identifier) ||
ItemTags.Any(tag => item.Tags.Contains(tag) || item.Identifier == tag);
lock (mutex)
{
return item.Prefab.GetAllowedUpgrades().Contains(Identifier) ||
ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag);
}
}
public static UpgradeCategory? Find(Identifier identifier)
@@ -104,6 +104,8 @@ namespace Barotrauma
(float)Math.Floor(value / div) * div;
}
public static int RoundToInt(float v) => (int)MathF.Round(v);
public static float RoundTowardsClosest(float value, float div)
{
return (float)Math.Round(value / div) * div;
@@ -0,0 +1,17 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma
{
[NetworkSerialize]
public readonly record struct NetCollection<T>(ImmutableArray<T> Array) : INetSerializableStruct, IEnumerable<T>
{
public static readonly NetCollection<T> Empty = new(ImmutableArray<T>.Empty);
public NetCollection(params T[] elements) : this(elements.ToImmutableArray()) { }
IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)Array).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Array).GetEnumerator();
}
}
@@ -18,7 +18,7 @@ namespace Barotrauma
#if OSX
//"/*user*/Library/Application Support/Daedalic Entertainment GmbH/" on Mac
public static readonly string SaveFolder = Path.Combine(
public static readonly string DefaultSaveFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
"Library",
"Application Support",
@@ -27,13 +27,13 @@ namespace Barotrauma
#else
//"C:/Users/*user*/AppData/Local/Daedalic Entertainment GmbH/" on Windows
//"/home/*user*/.local/share/Daedalic Entertainment GmbH/" on Linux
public static readonly string SaveFolder = Path.Combine(
public static readonly string DefaultSaveFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Daedalic Entertainment GmbH",
"Barotrauma");
#endif
public static string MultiplayerSaveFolder = Path.Combine(SaveFolder, "Multiplayer");
public static string DefaultMultiplayerSaveFolder = Path.Combine(DefaultSaveFolder, "Multiplayer");
public static readonly string SubmarineDownloadFolder = Path.Combine("Submarines", "Downloaded");
public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer_Downloaded");
@@ -43,9 +43,9 @@ namespace Barotrauma
public static string TempPath
{
#if SERVER
get { return Path.Combine(SaveFolder, "temp_server"); }
get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp_server"); }
#else
get { return Path.Combine(SaveFolder, "temp"); }
get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp"); }
#endif
}
@@ -198,7 +198,10 @@ namespace Barotrauma
}
//deleting a multiplayer save file -> also delete character data
if (Path.GetFullPath(Path.GetDirectoryName(filePath)).Equals(Path.GetFullPath(MultiplayerSaveFolder)))
var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath));
if (fullPath.Equals(Path.GetFullPath(DefaultMultiplayerSaveFolder)) ||
fullPath == Path.GetFullPath(GetSaveFolder(SaveType.Multiplayer)))
{
string characterDataSavePath = MultiPlayerCampaign.GetCharacterDataSavePath(filePath);
if (File.Exists(characterDataSavePath))
@@ -215,35 +218,70 @@ namespace Barotrauma
}
}
public static string GetSavePath(SaveType saveType, string saveName)
public static string GetSaveFolder(SaveType saveType)
{
string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder;
return Path.Combine(folder, saveName);
string folder = string.Empty;
if (!string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath))
{
folder = GameSettings.CurrentConfig.SavePath;
if (saveType == SaveType.Multiplayer)
{
folder = Path.Combine(folder, "Multiplayer");
}
if (!Directory.Exists(folder))
{
DebugConsole.AddWarning($"Could not find the custom save folder \"{folder}\", creating the folder...");
try
{
Directory.CreateDirectory(folder);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Could not find the custom save folder \"{folder}\". Using the default save path instead.", e);
folder = string.Empty;
}
}
}
if (string.IsNullOrEmpty(folder))
{
folder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder;
}
return folder;
}
public static IReadOnlyList<CampaignMode.SaveInfo> GetSaveFiles(SaveType saveType, bool includeInCompatible = true)
{
string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder;
if (!Directory.Exists(folder))
string defaultFolder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder;
if (!Directory.Exists(defaultFolder))
{
DebugConsole.Log("Save folder \"" + folder + " not found! Attempting to create a new folder...");
DebugConsole.Log("Save folder \"" + defaultFolder + " not found! Attempting to create a new folder...");
try
{
Directory.CreateDirectory(folder);
Directory.CreateDirectory(defaultFolder);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to create the folder \"" + folder + "\"!", e);
DebugConsole.ThrowError("Failed to create the folder \"" + defaultFolder + "\"!", e);
}
}
List<string> files = Directory.GetFiles(folder, "*.save", System.IO.SearchOption.TopDirectoryOnly).ToList();
List<string> files = Directory.GetFiles(defaultFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly).ToList();
var folder = GetSaveFolder(saveType);
if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder))
{
files.AddRange(Directory.GetFiles(folder, "*.save", System.IO.SearchOption.TopDirectoryOnly));
}
string legacyFolder = saveType == SaveType.Singleplayer ? LegacySaveFolder : LegacyMultiplayerSaveFolder;
if (Directory.Exists(legacyFolder))
{
files.AddRange(Directory.GetFiles(legacyFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly));
}
files = files.Distinct().ToList();
List<CampaignMode.SaveInfo> saveInfos = new List<CampaignMode.SaveInfo>();
foreach (string file in files)
{
@@ -305,7 +343,7 @@ namespace Barotrauma
{
fileName = ToolBox.RemoveInvalidFileNameChars(fileName);
string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder;
string folder = GetSaveFolder(saveType);
if (fileName == "Save_Default")
{
fileName = TextManager.Get("SaveFile.DefaultName").Value;
@@ -3,6 +3,7 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.IO;
using System.Linq;
@@ -101,7 +102,7 @@ namespace Barotrauma
string startPath = directory ?? "";
string saveFolder = SaveUtil.SaveFolder.Replace('\\', '/');
string saveFolder = SaveUtil.DefaultSaveFolder.Replace('\\', '/');
if (originalFilename.Replace('\\', '/').StartsWith(saveFolder))
{
//paths that lead to the save folder might have incorrect case,
@@ -721,5 +722,119 @@ namespace Barotrauma
{
return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value;
}
private static readonly ImmutableHashSet<char> affectedCharacters = ImmutableHashSet.Create('%', '+', '');
/// <summary>
/// Extends % and + characters to color tags in talent name tooltips to make them look nicer.
/// This obviously does not work in languages like French where a non breaking space is used
/// so it's just a a bit extra for the languages it works with.
/// </summary>
/// <param name="original"></param>
/// <returns></returns>
public static string ExtendColorToPercentageSigns(string original)
{
const string colorEnd = "‖color:end‖",
colorStart = "‖color:";
const char definitionIndicator = '‖';
char[] chars = original.ToCharArray();
for (int i = 0; i < chars.Length; i++)
{
if (!TryGetAt(i, chars, out char currentChar) || !affectedCharacters.Contains(currentChar)) { continue; }
// look behind
if (TryGetAt(i - 1, chars, out char c) && c is definitionIndicator)
{
int offset = colorEnd.Length;
if (MatchesSequence(i - offset, colorEnd, chars))
{
// push the color end tag forwards until the character is within the tag
char prev = currentChar;
for (int k = i - offset; k <= i; k++)
{
if (!TryGetAt(k, chars, out c)) { continue; }
chars[k] = prev;
prev = c;
}
continue;
}
}
// look ahead
if (TryGetAt(i + 1, chars, out c) && c is definitionIndicator)
{
if (!MatchesSequence(i + 1, colorStart, chars)) { continue; }
int offset = FindNextDefinitionOffset(i, colorStart.Length, chars);
// we probably reached the end of the string
if (offset > chars.Length) { continue; }
// push the color start tag back until the character is within the tag
char prev = currentChar;
for (int k = i + offset; k >= i; k--)
{
if (!TryGetAt(k, chars, out c)) { continue; }
chars[k] = prev;
prev = c;
}
// skip needlessly checking this section again since we already know what's ahead
i += offset;
}
}
static int FindNextDefinitionOffset(int index, int initialOffset, char[] chars)
{
int offset = initialOffset;
while (TryGetAt(index + offset, chars, out char c) && c is not definitionIndicator) { offset++; }
return offset;
}
static bool MatchesSequence(int index, string sequence, char[] chars)
{
for (int i = 0; i < sequence.Length; i++)
{
if (!TryGetAt(index + i, chars, out char c) || c != sequence[i]) { return false; }
}
return true;
}
static bool TryGetAt(int i, char[] chars, out char c)
{
if (i >= 0 && i < chars.Length)
{
c = chars[i];
return true;
}
c = default;
return false;
}
return new string(chars);
}
public static bool StatIdentifierMatches(Identifier original, Identifier match)
{
if (original == match) { return true; }
for (int i = 0; i < match.Value.Length; i++)
{
if (i >= original.Value.Length) { return match[i] is '~'; }
if (!CharEquals(original[i], match[i])) { return false; }
}
return false;
static bool CharEquals(char a, char b) => char.ToLowerInvariant(a) == char.ToLowerInvariant(b);
}
}
}