Unstable 1.2.4.0

This commit is contained in:
Markus Isberg
2023-11-30 13:53:00 +02:00
parent 8a2e2ea0ae
commit fb5ea537bf
210 changed files with 4201 additions and 1283 deletions
@@ -151,6 +151,19 @@ namespace Barotrauma
partial void InitProjSpecific(XElement element);
public Item FindEquippedItemByTag(Identifier tag)
{
if (tag.IsEmpty) { return null; }
for (int i = 0; i < slots.Length; i++)
{
if (SlotTypes[i] == InvSlotType.Any) { continue; }
var item = slots[i].FirstOrDefault();
if (item != null && item.HasTag(tag)) { return item; }
}
return null;
}
public int FindLimbSlot(InvSlotType limbSlot)
{
for (int i = 0; i < slots.Length; i++)
@@ -343,7 +343,12 @@ namespace Barotrauma.Items.Components
OnFailedToOpen();
return;
}
toggleCooldownTimer = ToggleCoolDown;
if (ToggleWhenClicked)
{
//do not activate cooldown at this point if the door doesn't get toggled when clicked
//(i.e. if it just sends out a signal that might get passed back to the door and try to toggle it)
toggleCooldownTimer = ToggleCoolDown;
}
if (IsStuck || IsJammed)
{
#if CLIENT
@@ -404,7 +409,7 @@ namespace Barotrauma.Items.Components
position.Y >= item.Rect.Y + Window.Y &&
position.Y <= item.Rect.Y + Window.Y + Window.Height &&
position.X >= item.Rect.X - maxPerpendicularDistance &&
position.Y <= item.Rect.Right + maxPerpendicularDistance;
position.X <= item.Rect.Right + maxPerpendicularDistance;
}
}
@@ -80,9 +80,6 @@ namespace Barotrauma.Items.Components
[Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public Vector2 OwnerSheetIndex { get; set; }
[Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool SpawnPointTagsGiven { get; set; }
public IdCard(Item item, ContentXElement element) : base(item, element) { }
public void Initialize(WayPoint spawnPoint, Character character)
@@ -439,9 +439,10 @@ namespace Barotrauma.Items.Components
if (targetItem.Removed) { return; }
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar)
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
{
Character.Controlled?.UpdateHUDProgressBar(targetItem,
Character.Controlled.UpdateHUDProgressBar(targetItem,
targetItem.WorldPosition,
targetItem.Condition / targetItem.MaxCondition,
emptyColor: GUIStyle.HealthBarColorLow,
@@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components
}
#if CLIENT
if (requiredTime < float.MaxValue)
if (requiredTime < float.MaxValue && picker == Character.Controlled)
{
Character.Controlled?.UpdateHUDProgressBar(
this,
@@ -572,8 +572,15 @@ namespace Barotrauma.Items.Components
structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureDamageMultiplier);
}
var didLeak = targetStructure.SectionIsLeakingFromOutside(sectionIndex);
targetStructure.AddDamage(sectionIndex, -structureFixAmount * degreeOfSuccess, user);
if (didLeak && !targetStructure.SectionIsLeakingFromOutside(sectionIndex))
{
user.CheckTalents(AbilityEffectType.OnRepairedOutsideLeak);
}
//if the next section is small enough, apply the effect to it as well
//(to make it easier to fix a small "left-over" section)
for (int i = -1; i < 2; i += 2)
@@ -660,9 +667,10 @@ namespace Barotrauma.Items.Components
float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier));
levelResource.DeattachTimer += addedDetachTime;
#if CLIENT
if (targetItem.Prefab.ShowHealthBar)
if (targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
{
Character.Controlled?.UpdateHUDProgressBar(
Character.Controlled.UpdateHUDProgressBar(
this,
targetItem.WorldPosition,
levelResource.DeattachTimer / levelResource.DeattachDuration,
@@ -60,12 +60,19 @@ namespace Barotrauma.Items.Components
set
{
if (parent == value) { return; }
if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; }
if (value != null) { value.OnActiveStateChanged += SetActiveState; }
if (InheritParentIsActive)
{
if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; }
if (value != null) { value.OnActiveStateChanged += SetActiveState; }
}
parent = value;
}
}
[Serialize(true, IsPropertySaveable.No, description: "If this is a child component of another component, should this component inherit the IsActive state of the parent?")]
public bool InheritParentIsActive { get; set; }
public readonly ContentXElement originalElement;
protected const float CorrectionDelay = 1.0f;
@@ -394,8 +401,11 @@ namespace Barotrauma.Items.Components
if (ic == null) { break; }
ic.Parent = this;
ic.IsActive = isActive;
OnActiveStateChanged += ic.SetActiveState;
if (ic.InheritParentIsActive)
{
ic.IsActive = isActive;
OnActiveStateChanged += ic.SetActiveState;
}
item.AddComponent(ic);
break;
@@ -236,6 +236,12 @@ namespace Barotrauma.Items.Components
get => Inventory.AllItems.Count(it => it.Condition > 0.0f);
}
public int ExtraStackSize
{
get => Inventory.ExtraStackSize;
set => Inventory.ExtraStackSize = value;
}
private readonly ImmutableArray<SlotRestrictions> slotRestrictions;
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
@@ -298,7 +304,10 @@ namespace Barotrauma.Items.Components
}
}
Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow);
// we have to assign this here because the fields are serialized before the inventory is created otherwise
ExtraStackSize = element.GetAttributeInt(nameof(ExtraStackSize), 0);
List<SlotRestrictions> newSlotRestrictions = new List<SlotRestrictions>(totalCapacity);
for (int i = 0; i < capacity; i++)
{
@@ -389,6 +398,7 @@ namespace Barotrauma.Items.Components
public void OnItemContained(Item containedItem)
{
int index = Inventory.FindIndex(containedItem);
RelatedItem relatedItem = null;
if (index >= 0 && index < slotRestrictions.Length)
{
if (slotRestrictions[index].ContainableItems != null)
@@ -397,6 +407,8 @@ namespace Barotrauma.Items.Components
foreach (var containableItem in slotRestrictions[index].ContainableItems)
{
if (!containableItem.MatchesItem(containedItem)) { continue; }
//the 1st matching ContainableItem of the slot determines the hiding, position and rotation of the item
relatedItem ??= containableItem;
foreach (StatusEffect effect in containableItem.StatusEffects)
{
activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition));
@@ -405,7 +417,6 @@ namespace Barotrauma.Items.Components
}
}
var relatedItem = FindContainableItem(containedItem);
var containedItemInfo = new ContainedItem(containedItem,
Hide: relatedItem?.Hide ?? false,
ItemPos: relatedItem?.ItemPos,
@@ -786,12 +797,9 @@ namespace Barotrauma.Items.Components
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;
int index = Inventory.FindIndex(item);
if (index == -1 ) { return null; }
return slotRestrictions[index]?.ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item));
}
/// <summary>
@@ -1095,6 +1103,7 @@ namespace Barotrauma.Items.Components
itemIds[i].Add(idRemap.GetOffsetId(id));
}
}
ExtraStackSize = componentElement.GetAttributeInt(nameof(ExtraStackSize), 0);
}
public override XElement Save(XElement parentElement)
@@ -1107,6 +1116,7 @@ namespace Barotrauma.Items.Components
itemIdStrings[i] = string.Join(';', items.Select(it => it.ID.ToString()));
}
componentElement.Add(new XAttribute("contained", string.Join(',', itemIdStrings)));
componentElement.Add(new XAttribute(nameof(ExtraStackSize), ExtraStackSize));
return componentElement;
}
}
@@ -104,7 +104,7 @@ namespace Barotrauma.Items.Components
// doesn't quite work properly, remaining time changes if tinkering stops
float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease);
float deconstructionSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed);
float deconstructionSpeed = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed);
if (DeconstructItemsSimultaneously)
{
@@ -149,13 +149,13 @@ namespace Barotrauma.Items.Components
{
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
}
currForce *= item.StatManager.GetAdjustedValue(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
currForce *= item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
{
currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease;
}
currForce = item.StatManager.GetAdjustedValue(ItemTalentStats.EngineSpeed, currForce);
currForce = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineSpeed, currForce);
//less effective when in a bad condition
currForce *= MathHelper.Lerp(0.5f, 2.0f, condition);
@@ -469,7 +469,7 @@ namespace Barotrauma.Items.Components
character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount);
}
user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount);
quality = GetFabricatedItemQuality(fabricatedItem, user);
quality = GetFabricatedItemQuality(fabricatedItem, user).RollQuality();
}
int amount = (int)fabricationitemAmount.Value;
@@ -534,12 +534,10 @@ namespace Barotrauma.Items.Components
{
foreach (Skill skill in fabricatedItem.RequiredSkills)
{
float userSkill = user.GetSkillLevel(skill.Identifier);
float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f);
float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill;
var addedSkillValue = new AbilityFabricatorSkillGain(skill.Identifier, addedSkill);
user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue);
user.Info.IncreaseSkillLevel(
user.Info.ApplySkillGain(
skill.Identifier,
addedSkillValue.Value);
}
@@ -576,10 +574,52 @@ namespace Barotrauma.Items.Components
return currPowerConsumption;
}
private static int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
public static float CalculateBonusRollPercentage(float skillLevel, float target)
=> Math.Clamp((skillLevel - target) / (100f - target) * 100f, min: 0, max: 100);
public readonly record struct QualityResult(int Quality, float PlusOnePercentage, float PlusTwoPercentage)
{
if (user?.Info == null) { return 0; }
if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; }
public static readonly QualityResult Empty = new QualityResult(0, 0, 0);
public bool HasRandomQualityRollChance => PlusOnePercentage > 0f || PlusTwoPercentage > 0f;
// The total real world percentage for a roll to succeed, taking into account that +1 needs to succeed for +2 to be attempted and
// that the chance for only +1 goes down as +2 increases since some of the +1's will turn into +2s
public float TotalPlusOnePercentage => Math.Clamp(PlusOnePercentage * (100f - PlusTwoPercentage) / 100f, min: 0, max: 100);
public float TotalPlusTwoPercentage => Math.Clamp(PlusOnePercentage * PlusTwoPercentage / 100f, min: 0, max: 100);
public int RollQuality()
{
int additionalQuality = 0;
if (Roll(PlusOnePercentage))
{
additionalQuality++;
if (Roll(PlusTwoPercentage))
{
additionalQuality++;
}
}
return Quality + additionalQuality;
static bool Roll(float percentage)
=> percentage >= Rand.Range(0, 100, Rand.RandSync.Unsynced);
}
}
public const int PlusOneQualityBonusThreshold = 50,
PlusTwoQualityBonusThreshold = 75;
public const int PlusOneTarget = 100,
PlusTwoTarget = 125;
public const float PlusOneLerp = 0.2f,
PlusTwoLerp = 0.4f;
private static QualityResult GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
{
if (user?.Info == null) { return QualityResult.Empty; }
if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return QualityResult.Empty; }
int quality = 0;
float floatQuality = 0.0f;
floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false);
@@ -593,34 +633,63 @@ namespace Barotrauma.Items.Components
}
quality = (int)floatQuality;
const int MaxCraftingSkill = 100;
// Use Option here instead of 0 because we want the lowest value and a value of 0 would always be lower than any other chance
Option<float> plusOne = Option.None,
plusTwo = Option.None;
//having a higher-than-100 skill (e.g. due to talents) gives +1 quality
quality += fabricatedItem.RequiredSkills.All(s => user.GetSkillLevel(s.Identifier) >= MaxCraftingSkill) ? 1 : 0;
foreach (var skill in fabricatedItem.RequiredSkills)
{
//+1 quality if the character's skill level is >20% from the min requirement towards max skill
//e.g. if the skill requirement is 10 -> 28
//40 -> 52
//90 -> 92
float skillRequirement = MathHelper.Lerp(skill.Level, MaxCraftingSkill, 0.2f);
if (user.GetSkillLevel(skill.Identifier) > skillRequirement)
float skillLevel = user.GetSkillLevel(skill.Identifier);
if (skillLevel >= PlusOneQualityBonusThreshold)
{
quality += 1;
//+1 quality chance if the character's skill level is >20% from the min requirement towards max skill as well as higher than 50
//e.g. if the skill requirement is 10 -> 28 (but minimum 50 threshold)
//40 -> 52
//90 -> 92
var bonusChance1 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusOneTarget, PlusOneLerp));
plusOne = OverrideChanceIfLess(plusOne, bonusChance1);
if (skillLevel >= PlusTwoQualityBonusThreshold)
{
var bonusChance2 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusTwoTarget, PlusTwoLerp));
plusTwo = OverrideChanceIfLess(plusTwo, bonusChance2);
}
else
{
break;
}
}
else
{
break;
}
static Option<float> OverrideChanceIfLess(Option<float> original, float bonusChance)
{
if (original.TryUnwrap(out var originalChance))
{
return originalChance > bonusChance ? Option.Some(bonusChance) : original;
}
return Option.Some(bonusChance);
}
}
return quality;
return new QualityResult(quality,
PlusOnePercentage: plusOne.Match(some: static f => f, none: static () => 0f),
PlusTwoPercentage: plusTwo.Match(some: static f => f, none: static () => 0f));
}
partial void UpdateRequiredTimeProjSpecific();
private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item)
{
return
return
(user != null && user.HasRecipeForItem(item.Identifier)) ||
GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier));
}
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
{
if (fabricableItem == null) { return false; }
@@ -698,7 +767,7 @@ namespace Barotrauma.Items.Components
//fabricating takes 100 times longer if degree of success is close to 0
//characters with a higher skill than required can fabricate up to 100% faster
float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f);
float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f);
if (user?.Info is { } info && fabricableItem.TargetItem is { } it)
{
@@ -138,14 +138,14 @@ namespace Barotrauma.Items.Components
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValue(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor;
currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
{
currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease;
}
currFlow = item.StatManager.GetAdjustedValue(ItemTalentStats.PumpSpeed, currFlow);
currFlow = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpSpeed, currFlow);
//less effective when in a bad condition
currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
@@ -875,7 +875,7 @@ namespace Barotrauma.Items.Components
}
}
private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate);
private float GetMaxOutput() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
private float GetFuelConsumption() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate);
}
}
@@ -336,8 +336,7 @@ namespace Barotrauma.Items.Components
{
showIceSpireWarning = false;
if (user != null && user.Info != null &&
user.SelectedItem == item &&
controlledSub != null && controlledSub.Velocity.LengthSquared() > 0.01f)
user.SelectedItem == item)
{
IncreaseSkillLevel(user, deltaTime);
}
@@ -402,14 +401,15 @@ namespace Barotrauma.Items.Components
private void IncreaseSkillLevel(Character user, float deltaTime)
{
if (controlledSub == null) { return; }
if (controlledSub.Velocity.LengthSquared() < 0.01f) { return; }
if (user?.Info == null) { return; }
// Do not increase the helm skill when "steering" the sub while docked into something static (e.g. outpost or wreck)
if (GameMain.GameSession?.Campaign != null && controlledSub != null && controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; }
if (GameMain.GameSession?.Campaign != null&& controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; }
float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f;
user.Info.IncreaseSkillLevel(
"helm".ToIdentifier(),
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime);
float speedMultiplier = MathHelper.Clamp(TargetVelocity.Length() / 100.0f, 0.0f, 1.0f);
user.Info.ApplySkillGain(Tags.HelmSkill,
SkillSettings.Current.SkillIncreasePerSecondWhenSteering * speedMultiplier * deltaTime);
}
private void UpdateAutoPilot(float deltaTime)
@@ -395,6 +395,6 @@ namespace Barotrauma.Items.Components
}
}
public float GetCapacity() => item.StatManager.GetAdjustedValue(ItemTalentStats.BatteryCapacity, Capacity);
public float GetCapacity() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.BatteryCapacity, Capacity);
}
}
@@ -454,6 +454,7 @@ namespace Barotrauma.Items.Components
base.RemoveComponentSpecific();
connectedRecipients?.Clear();
connectionDirty?.Clear();
recipientsToRefresh.Clear();
}
}
}
@@ -1017,9 +1017,10 @@ namespace Barotrauma.Items.Components
{
attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar)
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
(User == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
{
Character.Controlled?.UpdateHUDProgressBar(targetItem,
Character.Controlled.UpdateHUDProgressBar(targetItem,
targetItem.WorldPosition,
targetItem.Condition / targetItem.MaxCondition,
emptyColor: GUIStyle.HealthBarColorLow,
@@ -495,9 +495,7 @@ namespace Barotrauma.Items.Components
{
foreach (Skill skill in requiredSkills)
{
float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier);
CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier,
SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f));
CurrentFixer.Info?.ApplySkillGain(skill.Identifier, SkillSettings.Current.SkillIncreasePerRepair);
}
SteamAchievementManager.OnItemRepaired(item, CurrentFixer);
CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item));
@@ -570,7 +568,7 @@ namespace Barotrauma.Items.Components
if (item.ConditionPercentage > MinDeteriorationCondition)
{
float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
float deteriorationSpeed = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
if (ForceDeteriorationTimer > 0.0f) { deteriorationSpeed = Math.Max(deteriorationSpeed, 1.0f); }
item.Condition -= deteriorationSpeed * deltaTime;
}
@@ -157,7 +157,7 @@ namespace Barotrauma.Items.Components
delayedElementToLoad = Option.None;
}
private void LoadFromXML(ContentXElement loadElement)
public void LoadFromXML(ContentXElement loadElement)
{
foreach (var subElement in loadElement.Elements())
{
@@ -383,6 +383,12 @@ namespace Barotrauma.Items.Components
wire.RemoveConnection(item);
}
}
c.Grid = null;
}
foreach (var connection in Connections)
{
Powered.ChangedConnections.Remove(connection);
connection.Recipients.Clear();
}
Connections.Clear();
@@ -305,16 +305,17 @@ namespace Barotrauma.Items.Components
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
{
if (item.body != null && !item.body.Enabled)
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
}
else
if (item.body == null || item.body.Enabled ||
(item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems))
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
}
else
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
}
isOn = true;
SetLightSourceTransformProjSpecific();
base.IsActive = false;
@@ -341,8 +342,22 @@ namespace Barotrauma.Items.Components
#if CLIENT
Light.ParentSub = item.Submarine;
#endif
bool visibleInContainer;
var ownerCharacter = item.GetRootInventoryOwner() as Character;
if ((item.Container != null && ownerCharacter == null) ||
if (ownerCharacter != null && item.RootContainer?.GetComponent<Holdable>() is not { IsActive: true })
{
//if the item is in a character inventory, the light should only be visible if the character is holding the item
//(not if it's e.q. inside a wearable item, or in a rifle worn on the back)
visibleInContainer = false;
}
else
{
visibleInContainer = item.FindParentInventory(static it => it is ItemInventory { Container.HideItems: true }) == null;
}
if ((item.Container != null && !visibleInContainer && ownerCharacter == null) ||
(ownerCharacter != null && ownerCharacter.InvisibleTimer > 0.0f))
{
lightBrightness = 0.0f;
@@ -352,7 +367,7 @@ namespace Barotrauma.Items.Components
SetLightSourceTransformProjSpecific();
PhysicsBody body = ParentBody ?? item.body;
if (body != null && !body.Enabled)
if (body != null && !body.Enabled && !visibleInContainer)
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
@@ -432,6 +447,11 @@ namespace Barotrauma.Items.Components
target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness);
}
public override void Drop(Character dropper, bool setTransform = true)
{
SetLightSourceTransform();
}
partial void SetLightSourceState(bool enabled, float brightness);
public void SetLightSourceTransform()
@@ -275,6 +275,15 @@ namespace Barotrauma.Items.Components
}
}
protected override void RemoveComponentSpecific()
{
if (PhysicsBody != null)
{
PhysicsBody.Remove();
PhysicsBody = null;
}
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
base.ReceiveSignal(signal, connection);
@@ -61,6 +61,22 @@ namespace Barotrauma.Items.Components
private const float CrewAiFindTargetMaxInterval = 1.0f;
private const float CrewAIFindTargetMinInverval = 0.2f;
/// <summary>
/// Bots consider the projectile to move at least this fast when calculating how far ahead a moving target they need to aim.
/// Aiming ahead doesn't work reliably with very slow projectiles, because we'd need to take into account drag and gravity,
/// and the target would most likely move in a different direction anyway before the projectile reaches it.
/// </summary>
private const float MinimumProjectileVelocityForAimAhead = 20.0f;
/// <summary>
/// Bots don't try to aim ahead a moving target by more than this amount. If the target is very fast and/or the projectile very slow,
/// we'd need to aim so far ahead it'd most likely fail anyway.
/// </summary>
private const float MaximumAimAhead = 10.0f;
private float projectileSpeed;
private Item previousAmmo;
private int currentLoaderIndex;
private const float TinkeringPowerCostReduction = 0.2f;
@@ -563,8 +579,9 @@ namespace Barotrauma.Items.Components
// Do not increase the weapons skill when operating a turret in an outpost level
if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost))
{
user.Info.IncreaseSkillLevel("weapons".ToIdentifier(),
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f));
user.Info.ApplySkillGain(
Tags.WeaponsSkill,
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime);
}
float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f);
@@ -664,6 +681,11 @@ namespace Barotrauma.Items.Components
return GetAvailableInstantaneousBatteryPower() >= GetPowerRequiredToShoot();
}
private Vector2 GetBarrelDir()
{
return new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
}
private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false)
{
tryingToCharge = true;
@@ -709,7 +731,8 @@ namespace Barotrauma.Items.Components
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
if (projectileContainer != null && projectileContainer.Item != item)
{
projectileContainer?.Item.Use(deltaTime);
//user needs to be null because the ammo boxes shouldn't be directly usable by characters
projectileContainer?.Item.Use(deltaTime, user: null, userForOnUsedEvent: user);
}
}
else
@@ -735,7 +758,7 @@ namespace Barotrauma.Items.Components
ItemContainer projectileContainer = containerItem.GetComponent<ItemContainer>();
if (projectileContainer != null)
{
containerItem.Use(deltaTime);
containerItem.Use(deltaTime, user: null, userForOnUsedEvent: user);
projectiles = GetLoadedProjectiles();
if (projectiles.Any()) { return true; }
}
@@ -930,6 +953,7 @@ namespace Barotrauma.Items.Components
Projectile projectileComponent = projectile.GetComponent<Projectile>();
if (projectileComponent != null)
{
TryDetermineProjectileSpeed(projectileComponent);
projectileComponent.Launcher = item;
projectileComponent.Attacker = projectileComponent.User = user;
if (projectileComponent.Attack != null)
@@ -960,6 +984,16 @@ namespace Barotrauma.Items.Components
LaunchProjSpecific();
}
private void TryDetermineProjectileSpeed(Projectile projectile)
{
if (projectile != null && !projectile.Hitscan)
{
projectileSpeed =
ConvertUnits.ToDisplayUnits(
MathHelper.Clamp((projectile.LaunchImpulse + LaunchImpulse) / projectile.Item.body.Mass, MinimumProjectileVelocityForAimAhead, NetConfig.MaxPhysicsBodyVelocity));
}
}
partial void LaunchProjSpecific();
private static void ShiftItemsInProjectileContainer(ItemContainer container)
@@ -1143,7 +1177,7 @@ namespace Barotrauma.Items.Components
if (target is Hull targetHull)
{
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
Vector2 barrelDir = GetBarrelDir();
if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
{
return;
@@ -1244,8 +1278,24 @@ namespace Barotrauma.Items.Components
if (container != null)
{
maxProjectileCount += container.Capacity;
int projectiles = projectileContainer.ContainedItems.Count(it => it.Condition > 0.0f);
usableProjectileCount += projectiles;
var projectiles = projectileContainer.ContainedItems.Where(it => it.Condition > 0.0f);
var firstProjectile = projectiles.FirstOrDefault();
if (firstProjectile?.Prefab != previousAmmo?.Prefab)
{
//assume the projectiles are infinitely fast (no aiming ahead of the target) if we can't find projectiles to calculate the speed based on,
//and if the projectile type isn't the same as before
projectileSpeed = float.PositiveInfinity;
}
previousAmmo = firstProjectile;
if (projectiles.Any())
{
var projectile =
firstProjectile.GetComponent<Projectile>() ??
firstProjectile.ContainedItems.FirstOrDefault()?.GetComponent<Projectile>();
TryDetermineProjectileSpeed(projectile);
usableProjectileCount += projectiles.Count();
}
}
}
}
@@ -1409,6 +1459,7 @@ namespace Barotrauma.Items.Components
targetPos = currentTarget.WorldPosition;
}
bool iceSpireSpotted = false;
Vector2 targetVelocity = Vector2.Zero;
// Adjust the target character position (limb or submarine)
if (currentTarget is Character targetCharacter)
{
@@ -1424,20 +1475,39 @@ namespace Barotrauma.Items.Components
else
{
// Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head.
float closestDist = closestDistance;
float closestDistSqr = closestDistance;
foreach (Limb limb in targetCharacter.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.Hidden) { continue; }
if (!IsWithinAimingRadius(limb.WorldPosition)) { continue; }
float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
if (dist < closestDist)
float distSqr = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
if (distSqr < closestDistSqr)
{
closestDist = dist;
closestDistSqr = distSqr;
if (limb == targetCharacter.AnimController.MainLimb)
{
//prefer main limb (usually a much better target than the extremities that are often the closest limbs)
closestDistSqr *= 0.5f;
}
targetPos = limb.WorldPosition;
}
}
if (closestDist > shootDistance * shootDistance)
if (projectileSpeed < float.PositiveInfinity && targetPos.HasValue)
{
//lead the target (aim where the target will be in the future)
float dist = MathF.Sqrt(closestDistSqr);
float projectileMovementTime = dist / projectileSpeed;
targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity;
Vector2 movementAmount = targetVelocity * projectileMovementTime;
//don't try to compensate more than 10 meters - if the target is so fast or the projectile so slow we need to go beyond that,
//it'd most likely fail anyway
movementAmount = ConvertUnits.ToDisplayUnits(movementAmount.ClampLength(MaximumAimAhead));
Vector2 futurePosition = targetPos.Value + movementAmount;
targetPos = Vector2.Lerp(targetPos.Value, futurePosition, DegreeOfSuccess(character));
}
if (closestDistSqr > shootDistance * shootDistance)
{
aiFindTargetTimer = CrewAIFindTargetMinInverval;
ResetTarget();
@@ -1512,7 +1582,9 @@ namespace Barotrauma.Items.Components
if (targetPos == null) { return false; }
// Force the highest priority so that we don't change the objective while targeting enemies.
objective.ForceHighestPriority = true;
#if CLIENT
debugDrawTargetPos = targetPos.Value;
#endif
if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget)
{
if (character.IsOnPlayerTeam)
@@ -1563,8 +1635,28 @@ namespace Barotrauma.Items.Components
if (IsPointingTowards(targetPos.Value))
{
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
Vector2 barrelDir = GetBarrelDir();
Vector2 aimStartPos = item.WorldPosition;
Vector2 aimEndPos = item.WorldPosition + barrelDir * shootDistance;
bool allowShootingIfNothingInWay = false;
if (currentTarget != null)
{
Vector2 targetStartPos = currentTarget.WorldPosition;
Vector2 targetEndPos = currentTarget.WorldPosition + targetVelocity * ConvertUnits.ToDisplayUnits(MaximumAimAhead);
//if there's nothing in the way (not even the target we're trying to aim towards),
//shooting should only be allowed if we're aiming ahead of the target, in which case it's to be expected that we're aiming at "thin air"
allowShootingIfNothingInWay =
targetVelocity.LengthSquared() > 0.001f &&
MathUtils.LineSegmentsIntersect(
aimStartPos, aimEndPos,
targetStartPos, targetEndPos) &&
//target needs to be moving roughly perpendicular to us for aiming ahead of it to make sense
Math.Abs(Vector2.Dot(Vector2.Normalize(aimEndPos - aimStartPos), Vector2.Normalize(targetEndPos - targetStartPos))) < 0.5f;
}
Vector2 start = ConvertUnits.ToSimUnits(aimStartPos);
Vector2 end = ConvertUnits.ToSimUnits(aimEndPos);
// Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
Body worldTarget = CheckLineOfSight(start, end);
if (closestEnemy != null && closestEnemy.Submarine != null)
@@ -1572,11 +1664,13 @@ namespace Barotrauma.Items.Components
start -= closestEnemy.Submarine.SimPosition;
end -= closestEnemy.Submarine.SimPosition;
Body transformedTarget = CheckLineOfSight(start, end);
canShoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
canShoot =
CanShoot(transformedTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay) &&
(worldTarget == null || CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay));
}
else
{
canShoot = CanShoot(worldTarget, character);
canShoot = CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay);
}
if (!canShoot) { return false; }
if (character.IsOnPlayerTeam)
@@ -1666,9 +1760,13 @@ namespace Barotrauma.Items.Components
}
}
private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true)
private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true, bool allowShootingIfNothingInWay = false)
{
if (targetBody == null) { return false; }
if (targetBody == null)
{
//nothing in the way (not even the target we're trying to shoot) -> no point in firing at thin air
return allowShootingIfNothingInWay;
}
Character targetCharacter = null;
if (targetBody.UserData is Character c)
{
@@ -559,6 +559,7 @@ namespace Barotrauma.Items.Components
foreach (WearableSprite wearableSprite in wearableSprites)
{
wearableSprite?.Sprite?.Remove();
wearableSprite.Picker = null;
}
}
@@ -230,11 +230,19 @@ namespace Barotrauma
protected readonly int capacity;
protected readonly ItemSlot[] slots;
public bool Locked;
protected float syncItemsDelay;
private int extraStackSize;
public int ExtraStackSize
{
get => extraStackSize;
set => extraStackSize = MathHelper.Max(value, 0);
}
/// <summary>
/// All items contained in the inventory. Stacked items are returned as individual instances. DO NOT modify the contents of the inventory while enumerating this list.
/// </summary>
@@ -383,7 +383,7 @@ namespace Barotrauma
public float RotationRad { get; private set; }
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, IsPropertySaveable.Yes)]
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)]
public float Rotation
{
get
@@ -393,7 +393,7 @@ namespace Barotrauma
set
{
if (!Prefab.AllowRotatingInEditor) { return; }
RotationRad = MathHelper.ToRadians(value);
RotationRad = MathUtils.WrapAnglePi(MathHelper.ToRadians(value));
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen)
{
@@ -1327,8 +1327,11 @@ namespace Barotrauma
}
}
if (FlippedX) clone.FlipX(false);
if (FlippedY) clone.FlipY(false);
if (FlippedX) { clone.FlipX(false); }
if (FlippedY) { clone.FlipY(false); }
// Flipping an item tampers with its rotation, so restore it
clone.Rotation = Rotation;
foreach (ItemComponent component in clone.components)
{
@@ -1640,6 +1643,9 @@ namespace Barotrauma
return transformedRect;
}
public override Quad2D GetTransformedQuad()
=> Quad2D.FromSubmarineRectangle(rect).Rotated(-RotationRad);
/// <summary>
/// goes through every item and re-checks which hull they are in
/// </summary>
@@ -2516,7 +2522,7 @@ namespace Barotrauma
if (Prefab.AllowRotatingInEditor)
{
RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad);
RotationRad = MathUtils.WrapAnglePi(-RotationRad);
}
#if CLIENT
if (Prefab.CanSpriteFlipX)
@@ -2543,6 +2549,10 @@ namespace Barotrauma
return;
}
if (Prefab.AllowRotatingInEditor)
{
RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad);
}
#if CLIENT
if (Prefab.CanSpriteFlipY)
{
@@ -3043,7 +3053,10 @@ namespace Barotrauma
return -1;
}
public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null)
/// <param name="userForOnUsedEvent">User to pass to the OnUsed event. May need to be different than the user in cases like loaders using ammo boxes:
/// the box is technically being used by the loader, and doesn't allow a character to use it, but we may still need to know which character caused
/// the box to be used.</param>
public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null, Character userForOnUsedEvent = null)
{
if (RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim)))
{
@@ -3068,7 +3081,7 @@ namespace Barotrauma
ic.PlaySound(ActionType.OnUse, user);
#endif
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, user, targetLimb, useTarget: useTarget, user: user);
ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user));
ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user ?? userForOnUsedEvent));
if (ic.DeleteOnUse) { remove = true; }
}
}
@@ -3526,7 +3539,26 @@ namespace Barotrauma
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
if (!CanClientAccess(sender) || !(property.GetAttribute<ConditionallyEditable>()?.IsEditable(this) ?? true))
bool conditionAllowsEditing = true;
if (property.GetAttribute<ConditionallyEditable>() is { } condition)
{
conditionAllowsEditing = condition.IsEditable(this);
}
bool canAccess = false;
if (Container?.GetComponent<CircuitBox>() != null &&
Container.CanClientAccess(sender))
{
//items inside circuit boxes are inaccessible by "normal" means,
//but the properties can still be edited through the circuit box UI
canAccess = true;
}
else
{
canAccess = CanClientAccess(sender);
}
if (!canAccess || !conditionAllowsEditing)
{
allowEditing = false;
}
@@ -3799,6 +3831,11 @@ namespace Barotrauma
}
break;
}
case "itemstats":
{
item.StatManager.Load(subElement);
break;
}
default:
{
ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString());
@@ -3924,7 +3961,7 @@ namespace Barotrauma
foreach (ItemComponent component in item.components)
{
if (component.Parent != null) { component.IsActive = component.Parent.IsActive; }
if (component.Parent != null && component.InheritParentIsActive) { component.IsActive = component.Parent.IsActive; }
component.OnItemLoaded();
}
@@ -3994,6 +4031,8 @@ namespace Barotrauma
upgrade.Save(element);
}
statManager?.Save(element);
element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture)));
var conditionAttribute = element.GetAttribute("condition");
@@ -23,9 +23,10 @@ namespace Barotrauma
Upgrade = 8,
ItemStat = 9,
DroppedStack = 10,
SetHighlight = 11,
MinValue = 0,
MaxValue = 10
MaxValue = 11
}
public interface IEventData : NetEntityEvent.IData
@@ -871,24 +871,43 @@ namespace Barotrauma
public int GetMaxStackSize(Inventory inventory)
{
int extraStackSize = inventory switch
{
ItemInventory { Owner: Item it } i => (int)it.StatManager.GetAdjustedValueAdditive(ItemTalentStats.ExtraStackSize, i.ExtraStackSize),
CharacterInventory { Owner: Character { Info: { } info } } i => i.ExtraStackSize + (int)info.GetSavedStatValueWithAll(StatTypes.InventoryExtraStackSize, Category.ToIdentifier()),
not null => inventory.ExtraStackSize,
null => 0
};
if (inventory is CharacterInventory && maxStackSizeCharacterInventory > 0)
{
return maxStackSizeCharacterInventory;
return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize);
}
else if (inventory?.Owner is Item item &&
(item.GetComponent<Holdable>() is { Attachable: false } || item.GetComponent<Wearable>() != null))
{
if (maxStackSizeHoldableOrWearableInventory > 0)
{
return maxStackSizeHoldableOrWearableInventory;
return MaxStackWithExtra(maxStackSizeHoldableOrWearableInventory, extraStackSize);
}
else if (maxStackSizeCharacterInventory > 0)
{
//if maxStackSizeHoldableOrWearableInventory is not set, it defaults to maxStackSizeCharacterInventory
return maxStackSizeCharacterInventory;
return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize);
}
}
return maxStackSize;
return MaxStackWithExtra(maxStackSize, extraStackSize);
static int MaxStackWithExtra(int maxStackSize, int extraStackSize)
{
extraStackSize = Math.Max(extraStackSize, 0);
if (maxStackSize == 1)
{
return Math.Min(maxStackSize, Inventory.MaxPossibleStackSize);
}
return Math.Min(maxStackSize + extraStackSize, Inventory.MaxPossibleStackSize);
}
}
[Serialize(false, IsPropertySaveable.No)]
@@ -1138,10 +1157,13 @@ namespace Barotrauma
if (fabricationRecipes.TryGetValue(newRecipe.RecipeHash, out var prevRecipe))
{
//the errors below may be caused by a mod overriding a base item instead of this one, log the package of the base item in that case
var packageToLog = GetParentModPackageOrThisPackage();
var packageToLog =
(variantOf.ContentPackage != null && variantOf.ContentPackage != ContentPackageManager.VanillaCorePackage) ?
variantOf.ContentPackage :
GetParentModPackageOrThisPackage();
int prevRecipeIndex = loadedRecipes.IndexOf(prevRecipe);
DebugConsole.ThrowError(
DebugConsole.AddWarning(
$"Error in item prefab \"{ToString()}\": " +
$"Fabrication recipe #{loadedRecipes.Count + 1} has the same hash as recipe #{prevRecipeIndex + 1}. This is most likely caused by identical, duplicate recipes. " +
$"This will cause issues with fabrication.",
@@ -2,24 +2,46 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
[NetworkSerialize]
internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option<UInt32> UniqueCharacterId) : INetSerializableStruct
internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option<UInt32> UniqueCharacterId, bool Save) : INetSerializableStruct
{
/// <summary>
/// Stackable identifiers feature a unique ID to allow multiple stats applied by the same talent from different characters to coexist.
/// </summary>
public static TalentStatIdentifier CreateStackable(ItemTalentStats stat, Identifier talentIdentifier, UInt32 characterId)
=> new(stat, talentIdentifier, Option<UInt32>.Some(characterId));
=> new(stat, talentIdentifier, Option<UInt32>.Some(characterId), Save: false);
/// <summary>
/// Unstackable identifiers do not have a unique ID causing them to be identical to other stats applied by the same talent from different characters and thus only one of them will be applied.
/// <see cref="ItemStatManager.ApplyStat"/> will always use the highest value for unstackable stats.
/// </summary>
public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier)
=> new(stat, talentIdentifier, Option.None);
public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier, bool Save)
=> new(stat, talentIdentifier, Option.None, Save);
public XElement Serialize()
=> new XElement("Stat",
new XAttribute("type", Stat),
new XAttribute("talent", TalentIdentifier));
public static Option<TalentStatIdentifier> TryLoadFromXML(XElement element)
{
var stat = element.GetAttributeEnum("type", ItemTalentStats.None);
var talentIdentifier = element.GetAttributeIdentifier("talent", Identifier.Empty);
if (stat == ItemTalentStats.None || talentIdentifier == Identifier.Empty)
{
var error = $"Failed to load talent stat identifier from XML {element}";
DebugConsole.ThrowError(error);
GameAnalyticsManager.AddErrorEventOnce("ItemStatManager.TryLoadFromXML:Invalid", GameAnalyticsManager.ErrorSeverity.Error, error);
return Option.None;
}
return Option.Some(CreateUnstackable(stat, talentIdentifier, true));
}
}
internal sealed class ItemStatManager
@@ -29,14 +51,14 @@ namespace Barotrauma
public ItemStatManager(Item item) => this.item = item;
public void ApplyStat(ItemTalentStats stat, bool stackable, float value, CharacterTalent talent)
public void ApplyStat(ItemTalentStats stat, bool stackable, bool save, float value, CharacterTalent talent)
{
if (talent.Character?.ID is not { } characterId ||
talent.Prefab?.Identifier is not { } talentIdentifier) { return; }
var identifier = stackable
? TalentStatIdentifier.CreateStackable(stat, talentIdentifier, characterId)
: TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier);
: TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier, save);
if (!stackable)
{
@@ -57,12 +79,45 @@ namespace Barotrauma
#endif
}
public void Save(XElement parent)
{
var element = new XElement("itemstats");
foreach (var (key, value) in talentStats)
{
if (!key.Save) { continue; }
var statElement = key.Serialize();
statElement.Add(new XAttribute("value", value));
element.Add(statElement);
}
parent.Add(element);
}
public void Load(XElement element)
{
foreach (XElement statElement in element.Elements())
{
if (!TalentStatIdentifier.TryLoadFromXML(statElement).TryUnwrap(out var identifier)) { continue; }
var value = statElement.GetAttributeFloat("value", 0f);
ApplyStatDirect(identifier, value);
}
}
/// <summary>
/// Used for setting the value value from network packet; bypassing all validity checks.
/// </summary>
public void ApplyStatDirect(TalentStatIdentifier identifier, float value) => talentStats[identifier] = value;
public void ApplyStatDirect(TalentStatIdentifier identifier, float value)
=> talentStats[identifier] = value;
public float GetAdjustedValue(ItemTalentStats stat, float originalValue)
/// <summary>
/// Adjusts the value by multiplying it with the value of the talent stat
/// </summary>
public float GetAdjustedValueMultiplicative(ItemTalentStats stat, float originalValue)
{
float total = originalValue;
@@ -74,5 +129,21 @@ namespace Barotrauma
return total;
}
/// <summary>
/// Adjusts the value by adding the value of the talent stat instead of multiplying it
/// </summary>
public float GetAdjustedValueAdditive(ItemTalentStats stat, float originalValue)
{
float total = originalValue;
foreach (var (key, value) in talentStats)
{
if (key.Stat != stat) { continue; }
total += value;
}
return total;
}
}
}