Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop
This commit is contained in:
@@ -48,13 +48,13 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
private static string[] ParseSlotTypes(XElement element)
|
||||
private static string[] ParseSlotTypes(ContentXElement element)
|
||||
{
|
||||
string slotString = element.GetAttributeString("slots", null);
|
||||
return slotString == null ? Array.Empty<string>() : slotString.Split(',');
|
||||
}
|
||||
|
||||
public CharacterInventory(XElement element, Character character, bool spawnInitialItems)
|
||||
public CharacterInventory(ContentXElement element, Character character, bool spawnInitialItems)
|
||||
: base(character, ParseSlotTypes(element).Length)
|
||||
{
|
||||
this.character = character;
|
||||
@@ -73,7 +73,8 @@ namespace Barotrauma
|
||||
slotTypeNames[i] = slotTypeNames[i].Trim();
|
||||
if (!Enum.TryParse(slotTypeNames[i], out parsedSlotType))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in the inventory config of \"" + character.SpeciesName + "\" - " + slotTypeNames[i] + " is not a valid inventory slot type.");
|
||||
DebugConsole.ThrowError("Error in the inventory config of \"" + character.SpeciesName + "\" - " + slotTypeNames[i] + " is not a valid inventory slot type.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
SlotTypes[i] = parsedSlotType;
|
||||
switch (SlotTypes[i])
|
||||
@@ -99,7 +100,8 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError(
|
||||
$"Character \"{character.SpeciesName}\" is configured to spawn with so many items it will have less than 2 free inventory slots. " +
|
||||
"This can cause issues with talents that spawn extra loot in monsters' inventories."
|
||||
+ " Consider increasing the inventory size.");
|
||||
+ " Consider increasing the inventory size.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -115,7 +117,8 @@ namespace Barotrauma
|
||||
string itemIdentifier = subElement.GetAttributeString("identifier", "");
|
||||
if (!ItemPrefab.Prefabs.TryGet(itemIdentifier, out var itemPrefab))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found.");
|
||||
DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found.",
|
||||
contentPackage: element.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -131,7 +134,7 @@ namespace Barotrauma
|
||||
if (item != null && item.ParentInventory != this)
|
||||
{
|
||||
string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\".";
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
DebugConsole.ThrowError(errorMsg, contentPackage: element.ContentPackage);
|
||||
GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
}
|
||||
else if (!character.Enabled)
|
||||
@@ -148,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++)
|
||||
|
||||
@@ -184,6 +184,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public bool IsFullyClosed => IsClosed && OpenState <= 0f;
|
||||
|
||||
public bool HasWindow => Window != Rectangle.Empty;
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")]
|
||||
public bool HasIntegratedButtons { get; private set; }
|
||||
|
||||
@@ -341,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
|
||||
@@ -381,6 +388,31 @@ namespace Barotrauma.Items.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the given position inside the vertical bounds of the window, and roughly on the door horizontally? Or the other way around if the door opens horizontally.
|
||||
/// </summary>
|
||||
/// <param name="position">Position in the same coordinate space as the door.</param>
|
||||
/// <param name="maxPerpendicularDistance">Maximum horizontal distance from the door (or vertical if the door opens horizontally)</param>
|
||||
public bool IsPositionOnWindow(Vector2 position, float maxPerpendicularDistance = 10.0f)
|
||||
{
|
||||
if (IsHorizontal)
|
||||
{
|
||||
return
|
||||
position.X >= item.Rect.X + Window.X &&
|
||||
position.X <= item.Rect.X + Window.X + Window.Width &&
|
||||
position.Y >= item.Rect.Y - maxPerpendicularDistance &&
|
||||
position.Y <= item.Rect.Y - item.Rect.Height - maxPerpendicularDistance;
|
||||
}
|
||||
else
|
||||
{
|
||||
return
|
||||
position.Y >= item.Rect.Y + Window.Y &&
|
||||
position.Y <= item.Rect.Y + Window.Y + Window.Height &&
|
||||
position.X >= item.Rect.X - maxPerpendicularDistance &&
|
||||
position.X <= item.Rect.Right + maxPerpendicularDistance;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
UpdateProjSpecific(deltaTime);
|
||||
|
||||
@@ -226,7 +226,8 @@ namespace Barotrauma.Items.Components
|
||||
foreach ((Character character, Node node) in charactersInRange)
|
||||
{
|
||||
if (character == null || character.Removed) { continue; }
|
||||
character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor));
|
||||
character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor),
|
||||
impulseDirection: character.WorldPosition - node.WorldPosition);
|
||||
}
|
||||
}
|
||||
DischargeProjSpecific();
|
||||
|
||||
@@ -168,8 +168,8 @@ namespace Barotrauma.Items.Components
|
||||
conditionIncrease += user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f;
|
||||
if (item.Prefab == otherGeneticMaterial.item.Prefab)
|
||||
{
|
||||
float taintedProbability = GetTaintedProbabilityOnRefine(otherGeneticMaterial, user);
|
||||
item.Condition = Math.Max(item.Condition, otherGeneticMaterial.item.Condition) + conditionIncrease;
|
||||
float taintedProbability = GetTaintedProbabilityOnRefine(user);
|
||||
if (taintedProbability >= Rand.Range(0.0f, 1.0f))
|
||||
{
|
||||
MakeTainted();
|
||||
@@ -221,10 +221,10 @@ namespace Barotrauma.Items.Components
|
||||
return taintedEffectStrength;
|
||||
}
|
||||
|
||||
private float GetTaintedProbabilityOnRefine(Character user)
|
||||
private float GetTaintedProbabilityOnRefine(GeneticMaterial otherGeneticMaterial, Character user)
|
||||
{
|
||||
if (user == null) { return 1.0f; }
|
||||
float probability = MathHelper.Lerp(0.0f, 0.99f, item.Condition / 100.0f);
|
||||
float probability = MathHelper.Lerp(0.0f, 0.99f, Math.Max(item.Condition, otherGeneticMaterial.Item.Condition) / 100.0f);
|
||||
probability *= MathHelper.Lerp(1.0f, 0.25f, DegreeOfSuccess(user));
|
||||
return MathHelper.Clamp(probability, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ namespace Barotrauma.Items.Components
|
||||
StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name.Value);
|
||||
if (effect.type != ActionType.OnProduceSpawned)
|
||||
{
|
||||
DebugConsole.ThrowError("Only OnProduceSpawned type can be used in <ProducedItem>.");
|
||||
DebugConsole.ThrowError("Only OnProduceSpawned type can be used in <ProducedItem>.",
|
||||
contentPackage: element.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -440,9 +440,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,
|
||||
|
||||
@@ -134,7 +134,8 @@ namespace Barotrauma.Items.Components
|
||||
suitableProjectiles = element.GetAttributeIdentifierArray(nameof(suitableProjectiles), Array.Empty<Identifier>()).ToHashSet();
|
||||
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.");
|
||||
DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement.",
|
||||
item.Prefab.ContentPackage);
|
||||
}
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
@@ -137,7 +137,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (element.GetAttribute("limbfixamount") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute.");
|
||||
DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
|
||||
fixableEntities = new HashSet<Identifier>();
|
||||
@@ -149,7 +150,8 @@ namespace Barotrauma.Items.Components
|
||||
case "fixable":
|
||||
if (subElement.GetAttribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities.");
|
||||
DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities.",
|
||||
contentPackage: element.ContentPackage);
|
||||
fixableEntities.Add(subElement.GetAttribute("name").Value.ToIdentifier());
|
||||
}
|
||||
else
|
||||
@@ -536,7 +538,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * lastPickedFraction * 0.9f);
|
||||
if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; }
|
||||
new FireSource(displayPos);
|
||||
new FireSource(displayPos, sourceCharacter: user);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -570,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)
|
||||
@@ -658,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,
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (aimPos == Vector2.Zero)
|
||||
{
|
||||
aimPos = new Vector2(0.6f, 0.1f);
|
||||
aimPos = new Vector2(0.45f, 0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,12 +62,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;
|
||||
@@ -79,6 +86,12 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
public virtual bool DontTransferInventoryBetweenSubs => false;
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, the items inside any of the item containers on this item cannot be sold at an outpost.
|
||||
/// Use in similar cases as <see cref="DontTransferInventoryBetweenSubs"/>.
|
||||
/// </summary>
|
||||
public virtual bool DisallowSellingItemsFromContainer => false;
|
||||
|
||||
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes to pick up the item (in seconds).")]
|
||||
public float PickingTime
|
||||
{
|
||||
@@ -293,7 +306,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Invalid select key in " + element + "!", e);
|
||||
DebugConsole.ThrowError("Invalid select key in " + element + "!", e,
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
|
||||
PickKey = InputType.Select;
|
||||
@@ -306,7 +320,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Invalid pick key in " + element + "!", e);
|
||||
DebugConsole.ThrowError("Invalid pick key in " + element + "!", e,
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
@@ -318,7 +333,8 @@ namespace Barotrauma.Items.Components
|
||||
var component = item.Components.Find(ic => ic.Name.Equals(inheritRequiredSkillsFrom, StringComparison.OrdinalIgnoreCase));
|
||||
if (component == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its required skills from \"{inheritRequiredSkillsFrom}\", but a component of that type couldn't be found.");
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its required skills from \"{inheritRequiredSkillsFrom}\", but a component of that type couldn't be found.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -333,7 +349,8 @@ namespace Barotrauma.Items.Components
|
||||
var component = item.Components.Find(ic => ic.Name.Equals(inheritStatusEffectsFrom, StringComparison.OrdinalIgnoreCase));
|
||||
if (component == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its StatusEffects from \"{inheritStatusEffectsFrom}\", but a component of that type couldn't be found.");
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its StatusEffects from \"{inheritStatusEffectsFrom}\", but a component of that type couldn't be found.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
else if (component.statusEffectLists != null)
|
||||
{
|
||||
@@ -368,7 +385,8 @@ namespace Barotrauma.Items.Components
|
||||
case "requiredskills":
|
||||
if (subElement.GetAttribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill.");
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill.",
|
||||
contentPackage: element.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -385,8 +403,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;
|
||||
@@ -434,7 +455,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (!allowEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers.");
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,7 +998,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (errorMessages)
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})");
|
||||
DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -985,7 +1008,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (errorMessages)
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e);
|
||||
DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e,
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -998,14 +1022,16 @@ namespace Barotrauma.Items.Components
|
||||
if (constructor == null)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
$"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})");
|
||||
$"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})",
|
||||
contentPackage: element.ContentPackage);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
$"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e);
|
||||
$"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e,
|
||||
contentPackage: element.ContentPackage);
|
||||
return null;
|
||||
}
|
||||
ItemComponent ic = null;
|
||||
@@ -1018,7 +1044,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
catch (TargetInvocationException e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error while loading component of the type {type}.", e.InnerException);
|
||||
DebugConsole.ThrowError($"Error while loading component of the type {type}.", e.InnerException, contentPackage: element.ContentPackage);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
$"ItemComponent.Load:TargetInvocationException{item.Name}{element.Name}",
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
|
||||
@@ -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>();
|
||||
@@ -284,7 +290,8 @@ namespace Barotrauma.Items.Components
|
||||
RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name);
|
||||
if (containable == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.");
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.",
|
||||
contentPackage: element.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
ContainableItems ??= new List<RelatedItem>();
|
||||
@@ -297,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++)
|
||||
{
|
||||
@@ -321,7 +331,8 @@ namespace Barotrauma.Items.Components
|
||||
RelatedItem containable = RelatedItem.Load(subSubElement, returnEmpty: false, parentDebugName: item.Name);
|
||||
if (containable == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.");
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.",
|
||||
contentPackage: element.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
subContainableItems.Add(containable);
|
||||
@@ -349,7 +360,8 @@ namespace Barotrauma.Items.Components
|
||||
RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name);
|
||||
if (containable == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error when loading containable restrictions for \"" + item.Name + "\" - containable with no identifiers.");
|
||||
DebugConsole.ThrowError("Error when loading containable restrictions for \"" + item.Name + "\" - containable with no identifiers.",
|
||||
contentPackage: element.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
ContainableItems[containableIndex] = containable;
|
||||
@@ -386,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)
|
||||
@@ -394,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));
|
||||
@@ -402,7 +417,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
var relatedItem = FindContainableItem(containedItem);
|
||||
var containedItemInfo = new ContainedItem(containedItem,
|
||||
Hide: relatedItem?.Hide ?? false,
|
||||
ItemPos: relatedItem?.ItemPos,
|
||||
@@ -783,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>
|
||||
@@ -1092,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)
|
||||
@@ -1104,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,7 +622,7 @@ namespace Barotrauma.Items.Components
|
||||
return element;
|
||||
}
|
||||
|
||||
private void LoadLimbPositions(XElement element)
|
||||
private void LoadLimbPositions(ContentXElement element)
|
||||
{
|
||||
limbPositions.Clear();
|
||||
foreach (var subElement in element.Elements())
|
||||
@@ -631,7 +631,8 @@ namespace Barotrauma.Items.Components
|
||||
string limbStr = subElement.GetAttributeString("limb", "");
|
||||
if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type.");
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -101,7 +101,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator.");
|
||||
DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator.",
|
||||
contentPackage: element.ContentPackage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -119,12 +120,16 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
//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 = itemPrefab.GetParentModPackageOrThisPackage();
|
||||
|
||||
bool recipeInvalid = false;
|
||||
foreach (var requiredItem in recipe.RequiredItems)
|
||||
{
|
||||
if (requiredItem.ItemPrefabs.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\".");
|
||||
DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\".",
|
||||
contentPackage: packageToLog);
|
||||
recipeInvalid = true;
|
||||
}
|
||||
}
|
||||
@@ -132,7 +137,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (fabricationRecipes.TryGetValue(recipe.RecipeHash, out var duplicateRecipe))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\".");
|
||||
DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\".",
|
||||
contentPackage: packageToLog);
|
||||
continue;
|
||||
}
|
||||
fabricationRecipes.Add(recipe.RecipeHash, recipe);
|
||||
@@ -463,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;
|
||||
@@ -528,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);
|
||||
}
|
||||
@@ -570,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);
|
||||
@@ -587,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; }
|
||||
@@ -692,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,7 +407,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
if (!(this is RelayComponent))
|
||||
if (this is not RelayComponent)
|
||||
{
|
||||
if (PowerConnections.Any(p => !p.IsOutput) && PowerConnections.Any(p => p.IsOutput))
|
||||
{
|
||||
@@ -454,6 +454,7 @@ namespace Barotrauma.Items.Components
|
||||
base.RemoveComponentSpecific();
|
||||
connectedRecipients?.Clear();
|
||||
connectionDirty?.Clear();
|
||||
recipientsToRefresh.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +306,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (item.body == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!");
|
||||
DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!",
|
||||
contentPackage: element.ContentPackage);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1016,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,
|
||||
|
||||
@@ -60,7 +60,8 @@ namespace Barotrauma.Items.Components
|
||||
string statTypeString = subElement.GetAttributeString("stattype", "");
|
||||
if (!Enum.TryParse(statTypeString, true, out StatType statType))
|
||||
{
|
||||
DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + ((MapEntity)item).Prefab.Identifier + ")");
|
||||
DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + ((MapEntity)item).Prefab.Identifier + ")",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
float statValue = subElement.GetAttributeFloat("value", 0f);
|
||||
statValues.TryAdd(statType, statValue);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,8 @@ namespace Barotrauma.Items.Components
|
||||
Holdable = item.GetComponent<Holdable>();
|
||||
if (Holdable == null || !Holdable.Attachable)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in initializing a Scanner component: an attachable Holdable component is required on the same item and none was found");
|
||||
DebugConsole.ThrowError("Error in initializing a Scanner component: an attachable Holdable component is required on the same item and none was found",
|
||||
contentPackage: item.Prefab.ContentPackage);
|
||||
IsActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ namespace Barotrauma.Items.Components
|
||||
RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null);
|
||||
if (RequiredSignalCount < 1)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!");
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ namespace Barotrauma.Items.Components
|
||||
// We don't want the components and wires to transfer between subs as it would cause issues.
|
||||
public override bool DontTransferInventoryBetweenSubs => true;
|
||||
|
||||
// We don't want to sell the components and wires inside the circuit box
|
||||
public override bool DisallowSellingItemsFromContainer => true;
|
||||
|
||||
public Option<CircuitBoxConnection> FindInputOutputConnection(Identifier connectionName)
|
||||
{
|
||||
foreach (CircuitBoxInputConnection input in Inputs)
|
||||
@@ -154,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())
|
||||
{
|
||||
@@ -395,8 +398,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
Components.Add(new CircuitBoxComponent(id, spawnedItem, pos, this, usedResource));
|
||||
onItemSpawned?.Invoke(spawnedItem);
|
||||
OnViewUpdateProjSpecific();
|
||||
});
|
||||
|
||||
OnViewUpdateProjSpecific();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -97,7 +97,8 @@ namespace Barotrauma.Items.Components
|
||||
string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character");
|
||||
if (!Enum.TryParse(triggeredByAttribute, out triggeredBy))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type.");
|
||||
DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
triggerOnce = element.GetAttributeBool("triggeronce", false);
|
||||
string parentDebugName = $"TriggerComponent in {item.Name}";
|
||||
@@ -274,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)
|
||||
{
|
||||
|
||||
@@ -362,7 +362,8 @@ namespace Barotrauma.Items.Components
|
||||
case "sprite":
|
||||
if (subElement.GetAttribute("texture") == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Item \"" + item.Name + "\" doesn't have a texture specified!");
|
||||
DebugConsole.ThrowError("Item \"" + item.Name + "\" doesn't have a texture specified!",
|
||||
contentPackage: element.ContentPackage);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -558,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>
|
||||
@@ -1028,7 +1036,6 @@ namespace Barotrauma
|
||||
visualSlots[n].ShowBorderHighlight(Color.White, 0.1f, 0.4f);
|
||||
if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; }
|
||||
}
|
||||
syncItemsDelay = 1.0f;
|
||||
#endif
|
||||
CharacterHUD.RecreateHudTextsIfFocused(item);
|
||||
}
|
||||
|
||||
@@ -241,7 +241,21 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public Item RootContainer { get; private set; }
|
||||
private Item rootContainer;
|
||||
public Item RootContainer
|
||||
{
|
||||
get { return rootContainer; }
|
||||
private set
|
||||
{
|
||||
if (value == this)
|
||||
{
|
||||
DebugConsole.ThrowError($"Attempted to set the item \"{Prefab.Identifier}\" as it's own root container!\n{Environment.StackTrace.CleanupStackTrace()}");
|
||||
rootContainer = null;
|
||||
return;
|
||||
}
|
||||
rootContainer = value;
|
||||
}
|
||||
}
|
||||
|
||||
private bool inWaterProofContainer;
|
||||
|
||||
@@ -370,7 +384,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
|
||||
@@ -380,7 +394,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)
|
||||
{
|
||||
@@ -444,7 +458,7 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
if (scale == value) { return; }
|
||||
scale = MathHelper.Clamp(value, 0.01f, 10.0f);
|
||||
scale = MathHelper.Clamp(value, Prefab.MinScale, Prefab.MaxScale);
|
||||
|
||||
float relativeScale = scale / base.Prefab.Scale;
|
||||
|
||||
@@ -1078,7 +1092,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (!Physics.TryParseCollisionCategory(collisionCategoryStr, out Category cat))
|
||||
{
|
||||
DebugConsole.ThrowError("Invalid collision category in item \"" + Name + "\" (" + collisionCategoryStr + ")");
|
||||
DebugConsole.ThrowError("Invalid collision category in item \"" + Name + "\" (" + collisionCategoryStr + ")",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1233,7 +1248,8 @@ namespace Barotrauma
|
||||
var holdables = components.Where(c => c is Holdable);
|
||||
if (holdables.Count() > 1)
|
||||
{
|
||||
DebugConsole.AddWarning($"Item {Prefab.Identifier} has multiple {nameof(Holdable)} components ({string.Join(", ", holdables.Select(h => h.GetType().Name))}).");
|
||||
DebugConsole.AddWarning($"Item {Prefab.Identifier} has multiple {nameof(Holdable)} components ({string.Join(", ", holdables.Select(h => h.GetType().Name))}).",
|
||||
Prefab.ContentPackage);
|
||||
}
|
||||
|
||||
InsertToList();
|
||||
@@ -1314,8 +1330,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)
|
||||
{
|
||||
@@ -1627,6 +1646,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>
|
||||
@@ -1679,6 +1701,12 @@ namespace Barotrauma
|
||||
while (rootContainer.Container != null)
|
||||
{
|
||||
rootContainer = rootContainer.Container;
|
||||
if (rootContainer == this)
|
||||
{
|
||||
DebugConsole.ThrowError($"Invalid container hierarchy: \"{Prefab.Identifier}\" was contained inside itself!\n{Environment.StackTrace.CleanupStackTrace()}");
|
||||
rootContainer = null;
|
||||
break;
|
||||
}
|
||||
inWaterProofContainer |= rootContainer.WaterProof;
|
||||
}
|
||||
newRootContainer = rootContainer;
|
||||
@@ -1941,7 +1969,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true)
|
||||
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime,bool playSound = true)
|
||||
{
|
||||
if (Indestructible || InvulnerableToDamage) { return new AttackResult(); }
|
||||
|
||||
@@ -2497,7 +2525,7 @@ namespace Barotrauma
|
||||
|
||||
if (Prefab.AllowRotatingInEditor)
|
||||
{
|
||||
RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad);
|
||||
RotationRad = MathUtils.WrapAnglePi(-RotationRad);
|
||||
}
|
||||
#if CLIENT
|
||||
if (Prefab.CanSpriteFlipX)
|
||||
@@ -2524,6 +2552,10 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (Prefab.AllowRotatingInEditor)
|
||||
{
|
||||
RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad);
|
||||
}
|
||||
#if CLIENT
|
||||
if (Prefab.CanSpriteFlipY)
|
||||
{
|
||||
@@ -3024,7 +3056,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)))
|
||||
{
|
||||
@@ -3053,7 +3088,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; }
|
||||
}
|
||||
}
|
||||
@@ -3516,7 +3551,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;
|
||||
}
|
||||
@@ -3793,6 +3847,11 @@ namespace Barotrauma
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "itemstats":
|
||||
{
|
||||
item.StatManager.Load(subElement);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString());
|
||||
@@ -3918,7 +3977,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();
|
||||
}
|
||||
|
||||
@@ -3988,6 +4047,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
|
||||
|
||||
@@ -229,7 +229,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly int FabricationLimitMin, FabricationLimitMax;
|
||||
|
||||
public FabricationRecipe(XElement element, Identifier itemPrefab)
|
||||
public FabricationRecipe(ContentXElement element, Identifier itemPrefab)
|
||||
{
|
||||
TargetItemPrefabIdentifier = itemPrefab;
|
||||
var displayNameIdentifier = element.GetAttributeIdentifier("displayname", "");
|
||||
@@ -245,7 +245,8 @@ namespace Barotrauma
|
||||
OutCondition = element.GetAttributeFloat("outcondition", 1.0f);
|
||||
if (OutCondition > 1.0f)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error in \"{itemPrefab}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100}).");
|
||||
DebugConsole.AddWarning($"Error in \"{itemPrefab}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100}).",
|
||||
element.ContentPackage);
|
||||
}
|
||||
var requiredItems = new List<RequiredItem>();
|
||||
RequiresRecipe = element.GetAttributeBool("requiresrecipe", false);
|
||||
@@ -267,9 +268,10 @@ namespace Barotrauma
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "requiredskill":
|
||||
if (subElement.Attribute("name") != null)
|
||||
if (subElement.GetAttribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! Use skill identifiers instead of names.");
|
||||
DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! Use skill identifiers instead of names.",
|
||||
contentPackage: element.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -283,7 +285,8 @@ namespace Barotrauma
|
||||
Identifier requiredItemTag = subElement.GetAttributeIdentifier("tag", Identifier.Empty);
|
||||
if (requiredItemIdentifier == Identifier.Empty && requiredItemTag == Identifier.Empty)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! One of the required items has no identifier or tag.");
|
||||
DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! One of the required items has no identifier or tag.",
|
||||
contentPackage: element.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -819,7 +822,7 @@ namespace Barotrauma
|
||||
[Serialize(null, IsPropertySaveable.No)]
|
||||
public string EquipConfirmationText { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Can the item be rotated in the submarine editor.")]
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Can the item be rotated in the submarine editor?")]
|
||||
public bool AllowRotatingInEditor { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
@@ -830,7 +833,13 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No)]
|
||||
public bool CanFlipY { get; private set; }
|
||||
|
||||
|
||||
[Serialize(0.1f, IsPropertySaveable.No)]
|
||||
public float MinScale { get; private set; }
|
||||
|
||||
[Serialize(10.0f, IsPropertySaveable.No)]
|
||||
public float MaxScale { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
public bool IsDangerous { get; private set; }
|
||||
|
||||
@@ -862,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)]
|
||||
@@ -1016,7 +1044,8 @@ namespace Barotrauma
|
||||
|
||||
if (ConfigElement.GetAttribute("cargocontainername") != null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name.");
|
||||
DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name.",
|
||||
contentPackage: ConfigElement.ContentPackage);
|
||||
}
|
||||
|
||||
SerializableProperty.DeserializeProperties(this, ConfigElement);
|
||||
@@ -1054,7 +1083,8 @@ namespace Barotrauma
|
||||
if (subElement.GetAttribute("sourcerect") == null &&
|
||||
subElement.GetAttribute("sheetindex") == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!");
|
||||
DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!",
|
||||
contentPackage: ConfigElement.ContentPackage);
|
||||
}
|
||||
Size = Sprite.size;
|
||||
|
||||
@@ -1072,7 +1102,8 @@ namespace Barotrauma
|
||||
if (priceInfo.StoreIdentifier.IsEmpty) { continue; }
|
||||
if (storePrices.ContainsKey(priceInfo.StoreIdentifier))
|
||||
{
|
||||
DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the store \"{priceInfo.StoreIdentifier}\" defined more than once.");
|
||||
DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the store \"{priceInfo.StoreIdentifier}\" defined more than once.",
|
||||
ContentPackage);
|
||||
storePrices[priceInfo.StoreIdentifier] = priceInfo;
|
||||
}
|
||||
else
|
||||
@@ -1085,7 +1116,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (storePrices.ContainsKey(locationType))
|
||||
{
|
||||
DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the location type \"{locationType}\" defined more than once.");
|
||||
DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the location type \"{locationType}\" defined more than once.",
|
||||
ContentPackage);
|
||||
storePrices[locationType] = new PriceInfo(subElement);
|
||||
}
|
||||
else
|
||||
@@ -1103,13 +1135,15 @@ namespace Barotrauma
|
||||
{
|
||||
if (itemElement.Attribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items.");
|
||||
DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items.",
|
||||
contentPackage: ConfigElement.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
var deconstructItem = new DeconstructItem(itemElement, Identifier);
|
||||
if (deconstructItem.ItemIdentifier.IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier.");
|
||||
DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier.",
|
||||
contentPackage: ConfigElement.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
deconstructItems.Add(deconstructItem);
|
||||
@@ -1122,12 +1156,18 @@ namespace Barotrauma
|
||||
var newRecipe = new FabricationRecipe(subElement, Identifier);
|
||||
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 =
|
||||
(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."
|
||||
);
|
||||
$"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.",
|
||||
contentPackage: packageToLog);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1142,7 +1182,8 @@ namespace Barotrauma
|
||||
//it's ok for variants to clear the primary and secondary containers to disable the PreferredContainer element
|
||||
if (variantOf == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement}).");
|
||||
DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement}).",
|
||||
contentPackage: ConfigElement.ContentPackage);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1192,7 +1233,8 @@ namespace Barotrauma
|
||||
case "suitabletreatment":
|
||||
if (subElement.GetAttribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names.");
|
||||
DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names.",
|
||||
contentPackage: ConfigElement.ContentPackage);
|
||||
}
|
||||
Identifier treatmentIdentifier = subElement.GetAttributeIdentifier("identifier", subElement.GetAttributeIdentifier("type", Identifier.Empty));
|
||||
float suitability = subElement.GetAttributeFloat("suitability", 0.0f);
|
||||
@@ -1201,6 +1243,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
Size = ConfigElement.GetAttributeVector2(nameof(Size), Size);
|
||||
|
||||
#if CLIENT
|
||||
ParseSubElementsClient(ConfigElement, variantOf);
|
||||
#endif
|
||||
@@ -1231,7 +1275,7 @@ namespace Barotrauma
|
||||
|
||||
if (Sprite == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Item \"{ToString()}\" has no sprite!");
|
||||
DebugConsole.ThrowError($"Item \"{ToString()}\" has no sprite!", contentPackage: ConfigElement.ContentPackage);
|
||||
#if SERVER
|
||||
this.sprite = new Sprite("", Vector2.Zero);
|
||||
this.sprite.SourceRect = new Rectangle(0, 0, 32, 32);
|
||||
@@ -1248,7 +1292,8 @@ namespace Barotrauma
|
||||
if (Identifier == Identifier.Empty)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
$"Item prefab \"{ToString()}\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading.");
|
||||
$"Item prefab \"{ToString()}\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading.",
|
||||
contentPackage: ConfigElement.ContentPackage);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@@ -1256,7 +1301,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (!string.IsNullOrEmpty(OriginalName))
|
||||
{
|
||||
DebugConsole.AddWarning($"Item \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages.");
|
||||
DebugConsole.AddWarning($"Item \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages.",
|
||||
ContentPackage);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1316,9 +1362,9 @@ namespace Barotrauma
|
||||
{
|
||||
string message = $"Tried to get price info for \"{Identifier}\" with a null store parameter!\n{Environment.StackTrace.CleanupStackTrace()}";
|
||||
#if DEBUG
|
||||
DebugConsole.LogError(message);
|
||||
DebugConsole.LogError(message, contentPackage: ContentPackage);
|
||||
#else
|
||||
DebugConsole.AddWarning(message);
|
||||
DebugConsole.AddWarning(message, ContentPackage);
|
||||
GameAnalyticsManager.AddErrorEventOnce("ItemPrefab.GetPriceInfo:StoreParameterNull", GameAnalyticsManager.ErrorSeverity.Error, message);
|
||||
#endif
|
||||
return null;
|
||||
@@ -1525,6 +1571,9 @@ namespace Barotrauma
|
||||
|
||||
void CheckXML(XElement originalElement, XElement variantElement, XElement result)
|
||||
{
|
||||
//if either the parent or the variant are non-vanilla, assume the error is coming from that package
|
||||
var packageToLog = parent.ContentPackage != GameMain.VanillaContent ? parent.ContentPackage : ContentPackage;
|
||||
|
||||
if (result == null) { return; }
|
||||
if (result.Name.ToIdentifier() == "RequiredItem" &&
|
||||
result.Parent?.Name.ToIdentifier() == "Fabricate")
|
||||
@@ -1538,7 +1587,8 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " +
|
||||
$"the item inherits the fabrication requirement of x{originalAmount} \"{originalIdentifier}\" from the base item \"{parent.Identifier}\". " +
|
||||
$"If this is not intentional, you can use empty <RequiredItem /> elements in the item variant to remove any excess inherited fabrication requirements.");
|
||||
$"If this is not intentional, you can use empty <RequiredItem /> elements in the item variant to remove any excess inherited fabrication requirements.",
|
||||
packageToLog);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1549,7 +1599,8 @@ namespace Barotrauma
|
||||
DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " +
|
||||
$"the base item \"{parent.Identifier}\" requires x{originalAmount} \"{originalIdentifier}\" to fabricate. " +
|
||||
$"The variant only overrides the required item, not the amount, resulting in a requirement of x{originalAmount} \"{resultIdentifier}\". "+
|
||||
"Specify the amount in the variant to fix this.");
|
||||
"Specify the amount in the variant to fix this.",
|
||||
packageToLog);
|
||||
}
|
||||
}
|
||||
if (originalElement?.Name.ToIdentifier() == "Deconstruct" &&
|
||||
@@ -1559,18 +1610,35 @@ namespace Barotrauma
|
||||
variantElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem"))
|
||||
{
|
||||
DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " +
|
||||
$"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. Overriding the base recipe may not work correctly.");
|
||||
$"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. Overriding the base recipe may not work correctly.",
|
||||
packageToLog);
|
||||
}
|
||||
if (variantElement.Elements().Any(e => e.Name.ToIdentifier() == "Item") &&
|
||||
originalElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem"))
|
||||
{
|
||||
DebugConsole.AddWarning($"Potential error in item \"{parent.Identifier}\": " +
|
||||
$"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. The item variant \"{Identifier}\" may not override the base recipe correctly.");
|
||||
$"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. The item variant \"{Identifier}\" may not override the base recipe correctly.",
|
||||
packageToLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the base prefab this one is a variant of is defined in a non-vanilla package, returns that non-vanilla package.
|
||||
/// Otherwise returns the package of this prefab. Can be useful for logging errors that may have been caused by a mod overriding
|
||||
/// the base item.
|
||||
/// </summary>
|
||||
public ContentPackage GetParentModPackageOrThisPackage()
|
||||
{
|
||||
if (ParentPrefab != null &&
|
||||
ParentPrefab.ContentPackage != ContentPackageManager.VanillaCorePackage)
|
||||
{
|
||||
return ParentPrefab.ContentPackage;
|
||||
}
|
||||
return ContentPackage;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} (identifier: {Identifier})";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,7 +222,7 @@ namespace Barotrauma
|
||||
if (element.GetAttribute("name") != null)
|
||||
{
|
||||
//backwards compatibility + a console warning
|
||||
DebugConsole.ThrowError($"Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names.");
|
||||
DebugConsole.ThrowError($"Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names.", contentPackage: element.ContentPackage);
|
||||
Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty<Identifier>());
|
||||
//attempt to convert to identifiers and tags
|
||||
List<Identifier> convertedIdentifiers = new List<Identifier>();
|
||||
@@ -299,7 +299,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (!Enum.TryParse(typeStr, true, out type))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type.");
|
||||
DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type.", contentPackage: element.ContentPackage);
|
||||
type = RelationType.Invalid;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user