Release v0.15.12.0
This commit is contained in:
@@ -204,8 +204,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
target.InitializeLinks();
|
||||
|
||||
if (!item.linkedTo.Contains(target.item)) item.linkedTo.Add(target.item);
|
||||
if (!target.item.linkedTo.Contains(item)) target.item.linkedTo.Add(item);
|
||||
if (!item.linkedTo.Contains(target.item)) { item.linkedTo.Add(target.item); }
|
||||
if (!target.item.linkedTo.Contains(item)) { target.item.linkedTo.Add(item); }
|
||||
|
||||
if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target);
|
||||
if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this);
|
||||
@@ -291,7 +291,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
|
||||
List<MapEntity> removedEntities = item.linkedTo.Where(e => e.Removed).ToList();
|
||||
foreach (MapEntity removed in removedEntities) item.linkedTo.Remove(removed);
|
||||
foreach (MapEntity removed in removedEntities) { item.linkedTo.Remove(removed); }
|
||||
|
||||
if (!item.linkedTo.Any(e => e is Hull) && !DockingTarget.item.linkedTo.Any(e => e is Hull))
|
||||
{
|
||||
@@ -306,9 +306,8 @@ namespace Barotrauma.Items.Components
|
||||
if (myWayPoint != null && targetWayPoint != null)
|
||||
{
|
||||
myWayPoint.FindHull();
|
||||
myWayPoint.linkedTo.Add(targetWayPoint);
|
||||
targetWayPoint.FindHull();
|
||||
targetWayPoint.linkedTo.Add(myWayPoint);
|
||||
myWayPoint.ConnectTo(targetWayPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -597,8 +596,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
hullRects[i].X -= expand;
|
||||
hullRects[i].Width += expand * 2;
|
||||
hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition));
|
||||
hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition);
|
||||
hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]);
|
||||
hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch";
|
||||
hulls[i].AddToGrid(subs[i]);
|
||||
hulls[i].FreeID();
|
||||
|
||||
@@ -716,8 +716,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
hullRects[i].Y += expand;
|
||||
hullRects[i].Height += expand * 2;
|
||||
hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition));
|
||||
hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition);
|
||||
hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]);
|
||||
hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch";
|
||||
hulls[i].AddToGrid(subs[i]);
|
||||
hulls[i].FreeID();
|
||||
|
||||
@@ -873,8 +874,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
myWayPoint.FindHull();
|
||||
myWayPoint.linkedTo.Remove(targetWayPoint);
|
||||
myWayPoint.OnLinksChanged?.Invoke(myWayPoint);
|
||||
targetWayPoint.FindHull();
|
||||
targetWayPoint.linkedTo.Remove(myWayPoint);
|
||||
targetWayPoint.OnLinksChanged?.Invoke(targetWayPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1058,7 +1061,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.linkedTo.Any()) return;
|
||||
if (!item.linkedTo.Any()) { return; }
|
||||
|
||||
List<MapEntity> linked = new List<MapEntity>(item.linkedTo);
|
||||
foreach (MapEntity entity in linked)
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool isBroken;
|
||||
|
||||
public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck;
|
||||
public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck && !Impassable;
|
||||
|
||||
public bool IsBroken
|
||||
{
|
||||
|
||||
@@ -184,6 +184,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
foreach ((Character character, Node node) in charactersInRange)
|
||||
{
|
||||
if (character == null || character.Removed) { continue; }
|
||||
character.ApplyAttack(null, node.WorldPosition, attack, 1.0f);
|
||||
}
|
||||
}
|
||||
@@ -475,6 +476,21 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
switch (connection.Name)
|
||||
{
|
||||
case "activate":
|
||||
case "use":
|
||||
case "trigger_in":
|
||||
if (signal.value != "0")
|
||||
{
|
||||
item.Use(1.0f, null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
internal partial class EntitySpawnerComponent : ItemComponent, IDrawableComponent
|
||||
{
|
||||
public enum AreaShape
|
||||
{
|
||||
Rectangle,
|
||||
Circle
|
||||
}
|
||||
|
||||
[Editable, Serialize("", true, "Identifier of the item to spawn, does nothing if SpeciesName is set. Separate by comma to have multiple items spawn at random.")]
|
||||
public string? ItemIdentifier { get; set; }
|
||||
|
||||
[Editable, Serialize("", true, "Species name of the creature to spawn, takes priority if ItemIdentifier is set. Separate by comma to have multiple creatures spawn at random.")]
|
||||
public string? SpeciesName { get; set; }
|
||||
|
||||
[Editable, Serialize(true, true, "Only spawn if crew members are within certain area")]
|
||||
public bool OnlySpawnWhenCrewInRange { get; set; }
|
||||
|
||||
[Editable, Serialize(AreaShape.Rectangle, true, "Shape of the area where crew members need to stay")]
|
||||
public AreaShape CrewAreaShape { get; set; }
|
||||
|
||||
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", true, "Size of the rectangle where crew members need to stay. Does nothing if CrewAreaShape is set to Circle")]
|
||||
public Vector2 CrewAreaBounds { get; set; }
|
||||
|
||||
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Radius of the circle to spawn stuff in. Does nothing if CrewAreaShape is set to Rectangle")]
|
||||
public float CrewAreaRadius { get; set; }
|
||||
|
||||
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", true, "Offset of the crew area from the center of the item")]
|
||||
public Vector2 CrewAreaOffset { get; set; }
|
||||
|
||||
[Editable, Serialize(AreaShape.Rectangle, true, "Shape of the area where enemies or items are spawned")]
|
||||
public AreaShape SpawnAreaShape { get; set; }
|
||||
|
||||
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", true, "Size of the rectangle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Circle")]
|
||||
public Vector2 SpawnAreaBounds { get; set; }
|
||||
|
||||
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Radius of the circle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Rectangle")]
|
||||
public float SpawnAreaRadius { get; set; }
|
||||
|
||||
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", true, "Offset of the spawn area from the center of the item")]
|
||||
public Vector2 SpawnAreaOffset { get; set; }
|
||||
|
||||
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 1f), Serialize("10,40", true, "Time range between spawn attempts in seconds. Set both to a negative value to disable automatic spawning.")]
|
||||
public Vector2 SpawnTimerRange { get; set; }
|
||||
|
||||
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 1f, ValueStep = 1f, DecimalCount = 0), Serialize("1,3", true, "Minumum and maximum amount of items or creatures to spawn in one attempt")]
|
||||
public Vector2 SpawnAmountRange { get; set; }
|
||||
|
||||
[Editable(MinValueInt = int.MinValue, MaxValueInt = int.MaxValue), Serialize(8, true, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned")]
|
||||
public int MaximumAmount { get; set; }
|
||||
|
||||
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize(500f, true, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")]
|
||||
public float MaximumAmountRangePadding { get; set; }
|
||||
|
||||
[Serialize(true, true, "")]
|
||||
public bool CanSpawn { get; set; } = true;
|
||||
|
||||
private float SpawnTimer;
|
||||
private float? SpawnTimerGoal;
|
||||
|
||||
public EntitySpawnerComponent(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ItemIdentifier))
|
||||
{
|
||||
string[] allItems = ItemIdentifier.Split(',');
|
||||
foreach (string itemIdentifier in allItems)
|
||||
{
|
||||
string trimmedString = itemIdentifier.Trim();
|
||||
|
||||
bool found = false;
|
||||
|
||||
foreach (ItemPrefab prefab in ItemPrefab.Prefabs)
|
||||
{
|
||||
if (string.Equals(trimmedString, prefab.Identifier, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error loading {nameof(EntitySpawnerComponent)} - item prefab \"" + name + "\" (identifier \"" + trimmedString + "\") not found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.OnItemLoaded();
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
base.Update(deltaTime, cam);
|
||||
|
||||
item.SendSignal(CanSpawn ? "1" : "0", "state_out");
|
||||
|
||||
if (GameMain.NetworkMember is { IsClient: true }) { return; }
|
||||
|
||||
float minTime = Math.Min(SpawnTimerRange.X, SpawnTimerRange.Y),
|
||||
maxTime = Math.Max(SpawnTimerRange.X, SpawnTimerRange.Y);
|
||||
|
||||
if (minTime < 0 && maxTime < 0) { return; }
|
||||
|
||||
SpawnTimerGoal ??= Rand.Range(minTime, maxTime, Rand.RandSync.Unsynced);
|
||||
|
||||
SpawnTimer += deltaTime;
|
||||
|
||||
if (SpawnTimer > SpawnTimerGoal)
|
||||
{
|
||||
Spawn();
|
||||
SpawnTimerGoal = null;
|
||||
SpawnTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
bool isNonZero = signal.value != "0";
|
||||
bool isClient = GameMain.NetworkMember is { IsClient: true };
|
||||
|
||||
switch (connection.Name)
|
||||
{
|
||||
case "set_state":
|
||||
CanSpawn = isNonZero;
|
||||
break;
|
||||
case "toggle" when isNonZero:
|
||||
CanSpawn = !CanSpawn;
|
||||
break;
|
||||
case "trigger_in" when isNonZero && !isClient:
|
||||
Spawn();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private RectangleF GetAreaRectangle(Vector2 size, Vector2 offset, bool draw)
|
||||
{
|
||||
Vector2 pos = item.WorldPosition;
|
||||
if (draw)
|
||||
{
|
||||
pos.Y = -pos.Y;
|
||||
}
|
||||
|
||||
pos += offset;
|
||||
RectangleF rect = new RectangleF(pos.X - size.X / 2f, pos.Y - size.Y / 2f, size.X, size.Y);
|
||||
return rect;
|
||||
}
|
||||
|
||||
private bool CanSpawnMore()
|
||||
{
|
||||
if (!CanSpawn) { return false; }
|
||||
|
||||
if (OnlySpawnWhenCrewInRange)
|
||||
{
|
||||
if (!Character.CharacterList.Any(c => !c.IsDead && c.IsOnPlayerTeam && IsInRange(c.WorldPosition, crewArea: true, rangePad: false)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (MaximumAmount < 0) { return true; }
|
||||
|
||||
int amount;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(SpeciesName))
|
||||
{
|
||||
amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName.Equals(SpeciesName, StringComparison.OrdinalIgnoreCase) && IsInRange(c.WorldPosition, crewArea: false, rangePad: true));
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(ItemIdentifier))
|
||||
{
|
||||
amount = Item.ItemList.Count(it => it.Submarine == item.Submarine && it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.OrdinalIgnoreCase) && IsInRange(it.WorldPosition, crewArea: false, rangePad: true));
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return amount < MaximumAmount;
|
||||
}
|
||||
|
||||
private bool IsInRange(Vector2 worldPos, bool crewArea = false, bool rangePad = false)
|
||||
{
|
||||
Vector2 offset = crewArea ? CrewAreaOffset : SpawnAreaOffset;
|
||||
offset.Y = -offset.Y;
|
||||
switch (crewArea ? CrewAreaShape : SpawnAreaShape)
|
||||
{
|
||||
case AreaShape.Circle:
|
||||
Vector2 center = item.WorldPosition + offset;
|
||||
float distance = (crewArea ? CrewAreaRadius : SpawnAreaRadius) + (rangePad ? MaximumAmountRangePadding : 0);
|
||||
return Vector2.DistanceSquared(worldPos, center) < distance * distance;
|
||||
|
||||
case AreaShape.Rectangle:
|
||||
RectangleF rect = GetAreaRectangle(crewArea ? CrewAreaBounds : SpawnAreaBounds, offset, draw: false);
|
||||
if (rangePad)
|
||||
{
|
||||
rect.Inflate(MaximumAmountRangePadding, MaximumAmountRangePadding);
|
||||
}
|
||||
|
||||
return rect.Contains(worldPos);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Spawn()
|
||||
{
|
||||
if (!CanSpawnMore()) { return; }
|
||||
|
||||
int minAmount = Math.Min((int)SpawnAmountRange.X, (int)SpawnAmountRange.Y),
|
||||
maxAmount = Math.Max((int)SpawnAmountRange.X, (int)SpawnAmountRange.Y);
|
||||
|
||||
int amount = Rand.Range(minAmount, maxAmount, Rand.RandSync.Unsynced);
|
||||
|
||||
Vector2 offset = SpawnAreaOffset;
|
||||
offset.Y = -offset.Y;
|
||||
|
||||
switch (SpawnAreaShape)
|
||||
{
|
||||
case AreaShape.Circle:
|
||||
{
|
||||
var (x, y) = item.WorldPosition + offset;
|
||||
|
||||
for (int i = 0; i < Math.Max(1, amount); i++)
|
||||
{
|
||||
float angle = Rand.Range(-MathHelper.TwoPi, MathHelper.TwoPi);
|
||||
float distance = Rand.Range(0, SpawnAreaRadius, Rand.RandSync.Unsynced);
|
||||
Vector2 spawnPos = new Vector2(x + distance * (float)Math.Cos(angle), y + distance * (float)Math.Sin(angle));
|
||||
|
||||
SpawnEntity(spawnPos);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AreaShape.Rectangle:
|
||||
{
|
||||
RectangleF rect = GetAreaRectangle(SpawnAreaBounds, offset, draw: false);
|
||||
|
||||
for (int i = 0; i < Math.Max(1, amount); i++)
|
||||
{
|
||||
float minX = Math.Min(rect.Left, rect.Right),
|
||||
maxX = Math.Max(rect.Left, rect.Right),
|
||||
minY = Math.Min(rect.Top, rect.Bottom),
|
||||
maxY = Math.Max(rect.Top, rect.Bottom);
|
||||
|
||||
Vector2 spawnPos = new Vector2(Rand.Range(minX, maxX, Rand.RandSync.Unsynced), Rand.Range(minY, maxY, Rand.RandSync.Unsynced));
|
||||
|
||||
SpawnEntity(spawnPos);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SpawnEntity(Vector2 pos)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(SpeciesName))
|
||||
{
|
||||
string[] allSpecies = SpeciesName.Split(',');
|
||||
string species = allSpecies.GetRandom().Trim();
|
||||
Entity.Spawner?.AddToSpawnQueue(species, pos);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(ItemIdentifier))
|
||||
{
|
||||
string[] allItems = ItemIdentifier.Split(',');
|
||||
string itemIdentifier = allItems.GetRandom().Trim();
|
||||
ItemPrefab? prefab = ItemPrefab.Find(null, itemIdentifier);
|
||||
if (prefab is null) { return; }
|
||||
|
||||
if (item.Submarine is { } sub)
|
||||
{
|
||||
pos -= sub.Position;
|
||||
}
|
||||
|
||||
Entity.Spawner?.AddToSpawnQueue(prefab, pos, item.Submarine);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class GeneticMaterial : ItemComponent, IServerSerializable
|
||||
{
|
||||
private readonly string materialName;
|
||||
|
||||
private Character targetCharacter;
|
||||
private AfflictionPrefab selectedEffect, selectedTaintedEffect;
|
||||
|
||||
[Serialize("", true)]
|
||||
public string Effect
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("geneticmaterialdebuff", true)]
|
||||
public string TaintedEffect
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private bool tainted;
|
||||
[Serialize(false, true)]
|
||||
public bool Tainted
|
||||
{
|
||||
get { return tainted; }
|
||||
private set
|
||||
{
|
||||
if (!value) { return; }
|
||||
tainted = true;
|
||||
item.AllowDeconstruct = false;
|
||||
if (!string.IsNullOrEmpty(TaintedEffect))
|
||||
{
|
||||
selectedTaintedEffect = AfflictionPrefab.Prefabs.Where(a =>
|
||||
a.Identifier.Equals(TaintedEffect, StringComparison.OrdinalIgnoreCase) ||
|
||||
a.AfflictionType.Equals(TaintedEffect, StringComparison.OrdinalIgnoreCase)).GetRandom();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//only for saving the selected tainted effect
|
||||
[Serialize("", true)]
|
||||
public string SelectedTaintedEffect
|
||||
{
|
||||
get { return selectedTaintedEffect?.Identifier ?? string.Empty; }
|
||||
private set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) { return; }
|
||||
selectedTaintedEffect = AfflictionPrefab.Prefabs.Find(a => a.Identifier == value);
|
||||
}
|
||||
}
|
||||
|
||||
public GeneticMaterial(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
string nameId = element.GetAttributeString("nameidentifier", "");
|
||||
if (!string.IsNullOrEmpty(nameId))
|
||||
{
|
||||
materialName = TextManager.Get(nameId);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(Effect))
|
||||
{
|
||||
selectedEffect = AfflictionPrefab.Prefabs.Where(a =>
|
||||
a.Identifier.Equals(Effect, StringComparison.OrdinalIgnoreCase) ||
|
||||
a.AfflictionType.Equals(Effect, StringComparison.OrdinalIgnoreCase)).GetRandom();
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(3.0f, false)]
|
||||
public float ConditionIncreaseOnCombineMin { get; set; }
|
||||
|
||||
[Serialize(8.0f, false)]
|
||||
public float ConditionIncreaseOnCombineMax { get; set; }
|
||||
|
||||
public bool CanBeCombinedWith(GeneticMaterial otherGeneticMaterial)
|
||||
{
|
||||
return !tainted && otherGeneticMaterial != null && !otherGeneticMaterial.tainted && item.AllowDeconstruct && otherGeneticMaterial.item.AllowDeconstruct;
|
||||
}
|
||||
|
||||
public override void Equip(Character character)
|
||||
{
|
||||
if (character == null) { return; }
|
||||
IsActive = true;
|
||||
|
||||
if (targetCharacter != null) { return; }
|
||||
|
||||
if (tainted)
|
||||
{
|
||||
if (selectedTaintedEffect != null)
|
||||
{
|
||||
float selectedTaintedEffectStrength = item.ConditionPercentage / 100.0f * selectedTaintedEffect.MaxStrength;
|
||||
character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength));
|
||||
var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
|
||||
if (existingAffliction != null)
|
||||
{
|
||||
existingAffliction.Strength = selectedTaintedEffectStrength;
|
||||
}
|
||||
targetCharacter = character;
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (selectedEffect != null)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnWearing, 1.0f);
|
||||
float selectedEffectStrength = item.ConditionPercentage / 100.0f * selectedEffect.MaxStrength;
|
||||
character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength));
|
||||
var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
|
||||
if (existingAffliction != null)
|
||||
{
|
||||
existingAffliction.Strength = selectedEffectStrength;
|
||||
}
|
||||
targetCharacter = character;
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
foreach (Item containedItem in item.ContainedItems)
|
||||
{
|
||||
containedItem.GetComponent<GeneticMaterial>()?.Equip(character);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
base.Update(deltaTime, cam);
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
var rootContainer = item.GetRootContainer();
|
||||
if (!targetCharacter.HasEquippedItem(item) &&
|
||||
(rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface)))
|
||||
{
|
||||
item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, targetCharacter);
|
||||
targetCharacter.CharacterHealth.ReduceAffliction(null, selectedEffect.Identifier, selectedEffect.MaxStrength);
|
||||
if (tainted)
|
||||
{
|
||||
targetCharacter.CharacterHealth.ReduceAffliction(null, selectedTaintedEffect.Identifier, selectedTaintedEffect.MaxStrength);
|
||||
}
|
||||
targetCharacter = null;
|
||||
IsActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Combine(GeneticMaterial otherGeneticMaterial, Character user)
|
||||
{
|
||||
if (!CanBeCombinedWith(otherGeneticMaterial)) { return false; }
|
||||
|
||||
float conditionIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax);
|
||||
conditionIncrease += user.GetStatValue(StatTypes.GeneticMaterialRefineBonus);
|
||||
if (item.Prefab == otherGeneticMaterial.item.Prefab)
|
||||
{
|
||||
item.Condition = Math.Max(item.Condition, otherGeneticMaterial.item.Condition) + conditionIncrease;
|
||||
float taintedProbability = GetTaintedProbabilityOnRefine(user);
|
||||
if (taintedProbability >= Rand.Range(0.0f, 1.0f))
|
||||
{
|
||||
MakeTainted();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Condition = otherGeneticMaterial.Item.Condition =
|
||||
(item.Condition + otherGeneticMaterial.Item.Condition) / 2.0f + conditionIncrease;
|
||||
item.OwnInventory?.TryPutItem(otherGeneticMaterial.Item, user: null);
|
||||
item.AllowDeconstruct = false;
|
||||
otherGeneticMaterial.Item.AllowDeconstruct = false;
|
||||
if (GetTaintedProbabilityOnCombine(user) >= Rand.Range(0.0f, 1.0f))
|
||||
{
|
||||
MakeTainted();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private float GetTaintedProbabilityOnRefine(Character user)
|
||||
{
|
||||
if (user == null) { return 1.0f; }
|
||||
float probability = MathHelper.Lerp(0.0f, 0.99f, item.Condition / 100.0f);
|
||||
probability *= MathHelper.Lerp(1.0f, 0.25f, DegreeOfSuccess(user));
|
||||
return MathHelper.Clamp(probability, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
private float GetTaintedProbabilityOnCombine(Character user)
|
||||
{
|
||||
if (user == null) { return 1.0f; }
|
||||
float probability = 1.0f - user.GetStatValue(StatTypes.GeneticMaterialTaintedProbabilityReductionOnCombine);
|
||||
return MathHelper.Clamp(probability, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
private void MakeTainted()
|
||||
{
|
||||
if (GameMain.NetworkMember?.IsClient ?? false) { return; }
|
||||
Tainted = true;
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static string TryCreateName(ItemPrefab prefab, XElement element)
|
||||
{
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().Equals(nameof(GeneticMaterial), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string nameId = subElement.GetAttributeString("nameidentifier", "");
|
||||
if (!string.IsNullOrEmpty(nameId))
|
||||
{
|
||||
return prefab.Name.Replace("[type]", TextManager.Get(nameId, returnNull: true) ?? nameId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return prefab.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool attachable, attached, attachedByDefault;
|
||||
private Voronoi2.VoronoiCell attachTargetCell;
|
||||
private readonly PhysicsBody body;
|
||||
private PhysicsBody body;
|
||||
public PhysicsBody Pusher
|
||||
{
|
||||
get;
|
||||
@@ -79,6 +79,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Use the hand rotation instead of torso rotation for the item hold angle. Enable this if you want the item just to follow with the arm when not aiming instead of forcing the arm to a hold pose.")]
|
||||
public bool UseHandRotationForHoldAngle
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Can the item be attached to walls.")]
|
||||
public bool Attachable
|
||||
{
|
||||
@@ -93,6 +100,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Can the item only be attached in limited amount? Uses permanent stat values to check for legibility.")]
|
||||
public bool LimitedAttachable
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")]
|
||||
public bool AttachedByDefault
|
||||
{
|
||||
@@ -154,7 +168,8 @@ namespace Barotrauma.Items.Components
|
||||
BodyType = BodyType.Dynamic,
|
||||
CollidesWith = Physics.CollisionCharacter,
|
||||
CollisionCategories = Physics.CollisionItemBlocking,
|
||||
Enabled = false
|
||||
Enabled = false,
|
||||
UserData = "Holdable.Pusher"
|
||||
};
|
||||
Pusher.FarseerBody.OnCollision += OnPusherCollision;
|
||||
Pusher.FarseerBody.FixedRotation = false;
|
||||
@@ -205,7 +220,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
characterUsable = element.GetAttributeBool("characterusable", true);
|
||||
}
|
||||
|
||||
@@ -247,6 +261,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private void Drop(bool dropConnectedWires, Character dropper)
|
||||
{
|
||||
GetRope()?.Snap();
|
||||
if (dropConnectedWires)
|
||||
{
|
||||
DropConnectedWires(dropper);
|
||||
@@ -558,6 +573,15 @@ namespace Barotrauma.Items.Components
|
||||
PickKey = InputType.Select;
|
||||
}
|
||||
|
||||
public override void ParseMsg()
|
||||
{
|
||||
base.ParseMsg();
|
||||
if (Attachable)
|
||||
{
|
||||
prevMsg = DisplayMsg;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); }
|
||||
@@ -567,6 +591,28 @@ namespace Barotrauma.Items.Components
|
||||
if (!character.IsKeyDown(InputType.Aim)) { return false; }
|
||||
if (!CanBeAttached(character)) { return false; }
|
||||
|
||||
if (LimitedAttachable)
|
||||
{
|
||||
if (character?.Info == null)
|
||||
{
|
||||
DebugConsole.AddWarning("Character without CharacterInfo attempting to attach a limited attachable item!");
|
||||
return false;
|
||||
}
|
||||
Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true);
|
||||
Structure attachTarget = Structure.GetAttachTarget(attachPos);
|
||||
|
||||
int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
|
||||
int currentlyAttachedCount = Item.ItemList.Count(
|
||||
i => i.Submarine == attachTarget?.Submarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier);
|
||||
if (currentlyAttachedCount >= maxAttachableCount)
|
||||
{
|
||||
#if CLIENT
|
||||
GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
if (character != Character.Controlled)
|
||||
@@ -596,10 +642,13 @@ namespace Barotrauma.Items.Components
|
||||
item.Drop(character);
|
||||
item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f, findNewHull: false);
|
||||
}
|
||||
AttachToWall();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
AttachToWall();
|
||||
|
||||
public override bool SecondaryUse(float deltaTime, Character character = null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -661,6 +710,20 @@ namespace Barotrauma.Items.Components
|
||||
Update(deltaTime, cam);
|
||||
}
|
||||
|
||||
public Rope GetRope()
|
||||
{
|
||||
var rangedWeapon = Item.GetComponent<RangedWeapon>();
|
||||
if (rangedWeapon != null)
|
||||
{
|
||||
var lastProjectile = rangedWeapon.LastProjectile;
|
||||
if (lastProjectile != null)
|
||||
{
|
||||
return lastProjectile.Item.GetComponent<Rope>();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (attachTargetCell != null)
|
||||
@@ -715,9 +778,18 @@ namespace Barotrauma.Items.Components
|
||||
scaledHandlePos[1] = handlePos[1] * item.Scale;
|
||||
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim;
|
||||
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, aim, holdAngle);
|
||||
if (!aim)
|
||||
{
|
||||
var rope = GetRope();
|
||||
if (rope != null && rope.SnapWhenNotAimed && rope.Item.ParentInventory == null)
|
||||
{
|
||||
rope.Snap();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GetRope()?.Snap();
|
||||
Limb equipLimb = null;
|
||||
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.Headset) || picker.Inventory.IsInLimbSlot(item, InvSlotType.Head))
|
||||
{
|
||||
@@ -780,7 +852,19 @@ namespace Barotrauma.Items.Components
|
||||
DeattachFromWall();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
attachTargetCell = null;
|
||||
if (Pusher != null)
|
||||
{
|
||||
Pusher.Remove();
|
||||
Pusher = null;
|
||||
}
|
||||
body = null;
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
if (!attachable)
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class IdCard : Pickable
|
||||
{
|
||||
[Serialize(CharacterTeamType.None, true, alwaysUseInstanceValues: true)]
|
||||
public CharacterTeamType TeamID
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0, true, alwaysUseInstanceValues: true)]
|
||||
public int SubmarineSpecificID
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private JobPrefab cachedJobPrefab;
|
||||
private string cachedName;
|
||||
|
||||
public IdCard(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
|
||||
@@ -13,29 +31,33 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public void Initialize(CharacterInfo info)
|
||||
{
|
||||
if (info == null) return;
|
||||
if (info == null) { return; }
|
||||
|
||||
if (info.Job?.Prefab != null)
|
||||
{
|
||||
item.AddTag("jobid:" + info.Job.Prefab.Identifier);
|
||||
}
|
||||
|
||||
TeamID = info.TeamID;
|
||||
|
||||
var head = info.Head;
|
||||
|
||||
if (info != null && head != null)
|
||||
{
|
||||
item.AddTag("gender:" + head.gender.ToString().ToLowerInvariant());
|
||||
item.AddTag("race:" + head.race.ToString());
|
||||
item.AddTag("headspriteid:" + info.HeadSpriteId.ToString());
|
||||
item.AddTag("hairindex:" + head.HairIndex);
|
||||
item.AddTag("beardindex:" + head.BeardIndex);
|
||||
item.AddTag("moustacheindex:" + head.MoustacheIndex);
|
||||
item.AddTag("faceattachmentindex:" + head.FaceAttachmentIndex);
|
||||
if (head == null) { return; }
|
||||
|
||||
if (info.HasGenders) { item.AddTag($"gender:{head.gender.ToString().ToLowerInvariant()}"); }
|
||||
if (info.HasRaces) { item.AddTag($"race:{head.race}"); }
|
||||
item.AddTag($"headspriteid:{info.HeadSpriteId}");
|
||||
item.AddTag($"hairindex:{head.HairIndex}");
|
||||
item.AddTag($"beardindex:{head.BeardIndex}");
|
||||
item.AddTag($"moustacheindex:{head.MoustacheIndex}");
|
||||
item.AddTag($"faceattachmentindex:{head.FaceAttachmentIndex}");
|
||||
item.AddTag($"haircolor:{head.HairColor.ToStringHex()}");
|
||||
item.AddTag($"facialhaircolor:{head.FacialHairColor.ToStringHex()}");
|
||||
item.AddTag($"skincolor:{head.SkinColor.ToStringHex()}");
|
||||
|
||||
if (head.SheetIndex != null)
|
||||
{
|
||||
item.AddTag("sheetindex:" + head.SheetIndex.Value.X + ";" + head.SheetIndex.Value.Y);
|
||||
}
|
||||
if (head.SheetIndex != null)
|
||||
{
|
||||
item.AddTag($"sheetindex:{head.SheetIndex.Value.X};{head.SheetIndex.Value.Y}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,5 +72,48 @@ namespace Barotrauma.Items.Components
|
||||
base.Unequip(character);
|
||||
character.Info?.CheckDisguiseStatus(true, this);
|
||||
}
|
||||
|
||||
public JobPrefab GetJob()
|
||||
{
|
||||
if (cachedJobPrefab != null)
|
||||
{
|
||||
return cachedJobPrefab;
|
||||
}
|
||||
|
||||
foreach (string tag in item.GetTags())
|
||||
{
|
||||
if (tag.StartsWith("jobid:"))
|
||||
{
|
||||
string jobIdentifier = tag.Split(':').Last();
|
||||
if (JobPrefab.Get(jobIdentifier) is { } jobPrefab)
|
||||
{
|
||||
cachedJobPrefab = jobPrefab;
|
||||
return jobPrefab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetName()
|
||||
{
|
||||
if (cachedName != null)
|
||||
{
|
||||
return cachedName;
|
||||
}
|
||||
|
||||
foreach (string tag in item.GetTags())
|
||||
{
|
||||
if (tag.StartsWith("name:"))
|
||||
{
|
||||
string ownerName = tag.Split(':').Last();
|
||||
cachedName = ownerName;
|
||||
return ownerName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,17 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, false)]
|
||||
public bool Swing { get; set; }
|
||||
|
||||
[Editable, Serialize("2.0, 0.0", false)]
|
||||
public Vector2 SwingPos { get; set; }
|
||||
|
||||
[Editable, Serialize("3.0, -1.0", false)]
|
||||
public Vector2 SwingForce { get; set; }
|
||||
|
||||
public bool Hitting { get { return hitting; } }
|
||||
|
||||
/// <summary>
|
||||
/// Defines items that boost the weapon functionality, like battery cell for stun batons.
|
||||
/// </summary>
|
||||
@@ -61,12 +72,13 @@ namespace Barotrauma.Items.Components
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
Attack = new Attack(subElement, item.Name + ", MeleeWeapon");
|
||||
Attack.DamageRange = item.body == null ? 10.0f : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent());
|
||||
Attack = new Attack(subElement, item.Name + ", MeleeWeapon", item)
|
||||
{
|
||||
DamageRange = item.body == null ? 10.0f : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent())
|
||||
};
|
||||
}
|
||||
item.IsShootable = true;
|
||||
// TODO: should define this in xml if we have melee weapons that don't require aim to use
|
||||
item.RequireAimToUse = true;
|
||||
item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true);
|
||||
PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true);
|
||||
}
|
||||
|
||||
@@ -80,6 +92,9 @@ namespace Barotrauma.Items.Components
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
if (character == null || reloadTimer > 0.0f) { return false; }
|
||||
#if CLIENT
|
||||
if (!Item.RequireAimToUse && character.IsPlayer && (GUI.MouseOn != null || character.Inventory.visualSlots.Any(s => s.MouseOn()) || Inventory.DraggingItems.Any())) { return false; }
|
||||
#endif
|
||||
if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) { return false; }
|
||||
|
||||
//don't allow hitting if the character is already hitting with another weapon
|
||||
@@ -92,10 +107,10 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
SetUser(character);
|
||||
|
||||
if (hitPos < MathHelper.PiOver4) { return false; }
|
||||
if (Item.RequireAimToUse && hitPos < MathHelper.PiOver4) { return false; }
|
||||
|
||||
ActivateNearbySleepingCharacters();
|
||||
reloadTimer = reload;
|
||||
reloadTimer = reload / (1 + character.GetStatValue(StatTypes.MeleeAttackSpeed));
|
||||
|
||||
item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile;
|
||||
item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall;
|
||||
@@ -103,20 +118,28 @@ namespace Barotrauma.Items.Components
|
||||
item.body.FarseerBody.IsBullet = true;
|
||||
item.body.PhysEnabled = true;
|
||||
|
||||
if (!character.AnimController.InWater)
|
||||
if (Swing && !character.AnimController.InWater)
|
||||
{
|
||||
foreach (Limb l in character.AnimController.Limbs)
|
||||
{
|
||||
if (l.IsSevered) { continue; }
|
||||
if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) { continue; }
|
||||
if (l.type == LimbType.Head || l.type == LimbType.Torso)
|
||||
Vector2 force = new Vector2(character.AnimController.Dir * SwingForce.X, SwingForce.Y) * l.Mass;
|
||||
switch (l.type)
|
||||
{
|
||||
l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 7.0f, -4.0f));
|
||||
case LimbType.Torso:
|
||||
force *= 2;
|
||||
break;
|
||||
case LimbType.Legs:
|
||||
case LimbType.LeftFoot:
|
||||
case LimbType.LeftThigh:
|
||||
case LimbType.LeftLeg:
|
||||
case LimbType.RightFoot:
|
||||
case LimbType.RightThigh:
|
||||
case LimbType.RightLeg:
|
||||
force = Vector2.Zero;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 5.0f, -2.0f));
|
||||
}
|
||||
l.body.ApplyLinearImpulse(force);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,9 +175,16 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (!item.body.Enabled) { impactQueue.Clear(); return; }
|
||||
if (picker == null && !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; }
|
||||
|
||||
if (!item.body.Enabled)
|
||||
{
|
||||
impactQueue.Clear();
|
||||
return;
|
||||
}
|
||||
if (picker == null && !picker.HeldItems.Contains(item))
|
||||
{
|
||||
impactQueue.Clear();
|
||||
IsActive = false;
|
||||
}
|
||||
while (impactQueue.Count > 0)
|
||||
{
|
||||
var impact = impactQueue.Dequeue();
|
||||
@@ -162,38 +192,48 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
//in case handling the impact does something to the picker
|
||||
if (picker == null) { return; }
|
||||
|
||||
reloadTimer -= deltaTime;
|
||||
if (reloadTimer < 0) { reloadTimer = 0; }
|
||||
|
||||
if (!picker.IsKeyDown(InputType.Aim) && !hitting) { hitPos = 0.0f; }
|
||||
|
||||
if (reloadTimer < 0)
|
||||
{
|
||||
reloadTimer = 0;
|
||||
}
|
||||
if (!picker.IsKeyDown(InputType.Aim) && !hitting)
|
||||
{
|
||||
hitPos = 0.0f;
|
||||
}
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
|
||||
|
||||
if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); }
|
||||
|
||||
if (item.body.Dir != picker.AnimController.Dir)
|
||||
{
|
||||
item.FlipX(relativeToSub: false);
|
||||
}
|
||||
AnimController ac = picker.AnimController;
|
||||
|
||||
//TODO: refactor the hitting logic (get rid of the magic numbers, make it possible to use different kinds of animations for different items)
|
||||
if (!hitting)
|
||||
{
|
||||
bool aim = picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim;
|
||||
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim;
|
||||
if (aim)
|
||||
{
|
||||
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 5f, MathHelper.PiOver4));
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, hitPos, holdAngle + hitPos);
|
||||
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4));
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
hitPos = 0;
|
||||
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle);
|
||||
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
hitPos = MathUtils.WrapAnglePi(hitPos - deltaTime * 15f);
|
||||
ac.HoldItem(deltaTime, item, handlePos, new Vector2(2, 0), Vector2.Zero, false, hitPos, holdAngle + hitPos); // aimPos not used -> zero (new Vector2(-0.3f, 0.2f)), holdPos new Vector2(0.6f, -0.1f)
|
||||
if (hitPos < -MathHelper.PiOver2)
|
||||
// TODO: We might want to make this configurable
|
||||
hitPos -= deltaTime * 15f;
|
||||
if (Swing)
|
||||
{
|
||||
ac.HoldItem(deltaTime, item, handlePos, SwingPos, Vector2.Zero, aim: false, hitPos, holdAngle);
|
||||
}
|
||||
else
|
||||
{
|
||||
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
|
||||
}
|
||||
if (hitPos < -MathHelper.Pi)
|
||||
{
|
||||
RestoreCollision();
|
||||
hitting = false;
|
||||
@@ -266,16 +306,10 @@ namespace Barotrauma.Items.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
Character targetCharacter = null;
|
||||
Limb targetLimb = null;
|
||||
Structure targetStructure = null;
|
||||
Item targetItem = null;
|
||||
|
||||
if (f2.Body.UserData is Limb)
|
||||
if (f2.Body.UserData is Limb targetLimb)
|
||||
{
|
||||
targetLimb = (Limb)f2.Body.UserData;
|
||||
if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; }
|
||||
targetCharacter = targetLimb.character;
|
||||
var targetCharacter = targetLimb.character;
|
||||
if (targetCharacter == picker) { return false; }
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
@@ -287,9 +321,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
hitTargets.Add(targetCharacter);
|
||||
}
|
||||
else if (f2.Body.UserData is Character)
|
||||
else if (f2.Body.UserData is Character targetCharacter)
|
||||
{
|
||||
targetCharacter = (Character)f2.Body.UserData;
|
||||
if (targetCharacter == picker || targetCharacter == User) { return false; }
|
||||
targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
|
||||
if (AllowHitMultiple)
|
||||
@@ -302,9 +335,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
hitTargets.Add(targetCharacter);
|
||||
}
|
||||
else if (f2.Body.UserData is Structure)
|
||||
else if (f2.Body.UserData is Structure targetStructure)
|
||||
{
|
||||
targetStructure = (Structure)f2.Body.UserData;
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetStructure)) { return true; }
|
||||
@@ -315,9 +347,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
hitTargets.Add(targetStructure);
|
||||
}
|
||||
else if (f2.Body.UserData is Item)
|
||||
else if (f2.Body.UserData is Item targetItem)
|
||||
{
|
||||
targetItem = (Item)f2.Body.UserData;
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetItem)) { return true; }
|
||||
@@ -350,12 +381,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
Limb targetLimb = target.UserData as Limb;
|
||||
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
|
||||
Structure targetStructure = target.UserData as Structure;
|
||||
Item targetItem = target.UserData as Item;
|
||||
|
||||
if (Attack != null)
|
||||
{
|
||||
Attack.SetUser(User);
|
||||
Attack.DamageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier);
|
||||
Attack.DamageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.AttackMultiplier);
|
||||
|
||||
if (targetLimb != null)
|
||||
{
|
||||
@@ -369,12 +399,12 @@ namespace Barotrauma.Items.Components
|
||||
targetCharacter.LastDamageSource = item;
|
||||
Attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetStructure != null)
|
||||
else if (target.UserData is Structure targetStructure)
|
||||
{
|
||||
if (targetStructure.Removed) { return; }
|
||||
Attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
|
||||
else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
|
||||
{
|
||||
if (targetItem.Removed) { return; }
|
||||
Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -73,12 +74,15 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (PickingTime > 0.0f)
|
||||
{
|
||||
var abilityPickingTime = new AbilityValueItem(PickingTime, item.Prefab);
|
||||
picker.CheckTalents(AbilityEffectType.OnItemPicked, abilityPickingTime);
|
||||
|
||||
if ((picker.PickingItem == null || picker.PickingItem == item) && PickingTime <= float.MaxValue)
|
||||
{
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
pickingCoroutine = CoroutineManager.StartCoroutine(WaitForPick(picker, PickingTime));
|
||||
pickingCoroutine = CoroutineManager.StartCoroutine(WaitForPick(picker, abilityPickingTime.Value));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -66,18 +66,18 @@ namespace Barotrauma.Items.Components
|
||||
foreach (Limb limb in character.AnimController.Limbs)
|
||||
{
|
||||
if (limb.WearingItems.Find(w => w.WearableComponent.Item == item) == null) { continue; }
|
||||
limb.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
limb.body.ApplyForce(propulsion);
|
||||
}
|
||||
|
||||
character.AnimController.Collider.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
character.AnimController.Collider.ApplyForce(propulsion);
|
||||
|
||||
if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand))
|
||||
{
|
||||
character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion);
|
||||
}
|
||||
if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand))
|
||||
{
|
||||
character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Collision;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -52,17 +53,38 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0f, true, description: "The time required for a charge-type turret to charge up before able to fire.")]
|
||||
public float MaxChargeTime
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private enum ChargingState
|
||||
{
|
||||
Inactive,
|
||||
WindingUp,
|
||||
WindingDown,
|
||||
}
|
||||
private ChargingState currentChargingState;
|
||||
|
||||
public Vector2 TransformedBarrelPos
|
||||
{
|
||||
get
|
||||
{
|
||||
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation);
|
||||
Vector2 flippedPos = barrelPos;
|
||||
if (item.body.Dir < 0.0f) flippedPos.X = -flippedPos.X;
|
||||
return Vector2.Transform(flippedPos, bodyTransform);
|
||||
if (item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; }
|
||||
return Vector2.Transform(flippedPos, bodyTransform) * item.Scale;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Projectile LastProjectile { get; private set; }
|
||||
|
||||
private float currentChargeTime;
|
||||
private bool tryingToCharge;
|
||||
|
||||
public RangedWeapon(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -88,10 +110,41 @@ namespace Barotrauma.Items.Components
|
||||
if (ReloadTimer < 0.0f)
|
||||
{
|
||||
ReloadTimer = 0.0f;
|
||||
IsActive = false;
|
||||
// was this an optimization or related to something else? it cannot occur for charge-type weapons
|
||||
//IsActive = false;
|
||||
if (MaxChargeTime == 0.0f)
|
||||
{
|
||||
IsActive = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
float previousChargeTime = currentChargeTime;
|
||||
|
||||
float chargeDeltaTime = tryingToCharge && ReloadTimer <= 0f ? deltaTime : -deltaTime;
|
||||
currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime);
|
||||
|
||||
tryingToCharge = false;
|
||||
|
||||
if (currentChargeTime == 0f)
|
||||
{
|
||||
currentChargingState = ChargingState.Inactive;
|
||||
}
|
||||
else if (currentChargeTime < previousChargeTime)
|
||||
{
|
||||
currentChargingState = ChargingState.WindingDown;
|
||||
}
|
||||
else
|
||||
{
|
||||
// if we are charging up or at maxed charge, remain winding up
|
||||
currentChargingState = ChargingState.WindingUp;
|
||||
}
|
||||
|
||||
UpdateProjSpecific(deltaTime);
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
private float GetSpread(Character user)
|
||||
{
|
||||
float degreeOfFailure = 1.0f - DegreeOfSuccess(user);
|
||||
@@ -102,11 +155,20 @@ namespace Barotrauma.Items.Components
|
||||
private readonly List<Body> limbBodies = new List<Body>();
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
tryingToCharge = true;
|
||||
if (character == null || character.Removed) { return false; }
|
||||
if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || ReloadTimer > 0.0f) { return false; }
|
||||
if (currentChargeTime < MaxChargeTime) { return false; }
|
||||
|
||||
IsActive = true;
|
||||
ReloadTimer = reload;
|
||||
ReloadTimer = reload / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
|
||||
currentChargeTime = 0f;
|
||||
|
||||
if (character != null)
|
||||
{
|
||||
var abilityItem = new AbilityItem(item);
|
||||
character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityItem);
|
||||
}
|
||||
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
@@ -136,7 +198,13 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
|
||||
float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi;
|
||||
float spread = GetSpread(character) * Rand.Range(-0.5f, 0.5f);
|
||||
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false);
|
||||
var lastProjectile = LastProjectile;
|
||||
if (lastProjectile != projectile)
|
||||
{
|
||||
lastProjectile?.Item.GetComponent<Rope>()?.Snap();
|
||||
}
|
||||
float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.AttackMultiplier);
|
||||
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false, damageMultiplier);
|
||||
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
|
||||
if (i == 0)
|
||||
{
|
||||
@@ -145,6 +213,7 @@ namespace Barotrauma.Items.Components
|
||||
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f));
|
||||
Item.RemoveContained(projectile.Item);
|
||||
}
|
||||
LastProjectile = projectile;
|
||||
}
|
||||
|
||||
LaunchProjSpecific();
|
||||
|
||||
@@ -523,7 +523,20 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetStructure });
|
||||
FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex);
|
||||
targetStructure.AddDamage(sectionIndex, -StructureFixAmount * degreeOfSuccess, user);
|
||||
|
||||
float structureFixAmount = StructureFixAmount;
|
||||
if (structureFixAmount >= 0f)
|
||||
{
|
||||
structureFixAmount *= 1 + user.GetStatValue(StatTypes.RepairToolStructureRepairMultiplier);
|
||||
structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureRepairMultiplier);
|
||||
}
|
||||
else
|
||||
{
|
||||
structureFixAmount *= 1 + user.GetStatValue(StatTypes.RepairToolStructureDamageMultiplier);
|
||||
structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureDamageMultiplier);
|
||||
}
|
||||
|
||||
targetStructure.AddDamage(sectionIndex, -structureFixAmount * degreeOfSuccess, user);
|
||||
|
||||
//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)
|
||||
@@ -535,7 +548,7 @@ namespace Barotrauma.Items.Components
|
||||
(nextSectionLength > 0 && nextSectionLength < Structure.WallSectionSize * 0.3f))
|
||||
{
|
||||
//targetStructure.HighLightSection(sectionIndex + i);
|
||||
targetStructure.AddDamage(sectionIndex + i, -StructureFixAmount * degreeOfSuccess);
|
||||
targetStructure.AddDamage(sectionIndex + i, -structureFixAmount * degreeOfSuccess);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -606,7 +619,8 @@ namespace Barotrauma.Items.Components
|
||||
levelResource.requiredItems.Any() &&
|
||||
levelResource.HasRequiredItems(user, addMessage: false))
|
||||
{
|
||||
levelResource.DeattachTimer += deltaTime;
|
||||
float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier));
|
||||
levelResource.DeattachTimer += addedDetachTime;
|
||||
#if CLIENT
|
||||
Character.Controlled?.UpdateHUDProgressBar(
|
||||
this,
|
||||
@@ -698,7 +712,7 @@ namespace Barotrauma.Items.Components
|
||||
humanAnim.Crouching = true;
|
||||
}
|
||||
}
|
||||
if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.inWater))
|
||||
if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater))
|
||||
{
|
||||
// Steer closer
|
||||
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
|
||||
|
||||
@@ -123,18 +123,18 @@ namespace Barotrauma.Items.Components
|
||||
if (aim)
|
||||
{
|
||||
throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2));
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos);
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
throwPos = 0;
|
||||
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle);
|
||||
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f);
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos);
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos);
|
||||
|
||||
if (throwPos < 0)
|
||||
{
|
||||
@@ -169,8 +169,8 @@ namespace Barotrauma.Items.Components
|
||||
item.body.FarseerBody.IsBullet = true;
|
||||
midAir = true;
|
||||
|
||||
ac.GetLimb(LimbType.Head).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
ac.GetLimb(LimbType.Torso).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
ac.GetLimb(LimbType.Head)?.body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
ac.GetLimb(LimbType.Torso)?.body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
|
||||
Limb rightHand = ac.GetLimb(LimbType.RightHand);
|
||||
item.body.AngularVelocity = rightHand.body.AngularVelocity;
|
||||
|
||||
@@ -711,7 +711,27 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
bool CheckItems(RelatedItem relatedItem, IEnumerable<Item> itemList)
|
||||
{
|
||||
bool Predicate(Item it) => it != null && it.Condition > 0.0f && relatedItem.MatchesItem(it);
|
||||
bool Predicate(Item it)
|
||||
{
|
||||
if (it == null || it.Condition <= 0.0f || !relatedItem.MatchesItem(it)) { return false; }
|
||||
if (item.Submarine != null)
|
||||
{
|
||||
var idCard = it.GetComponent<IdCard>();
|
||||
if (idCard != null)
|
||||
{
|
||||
//id cards don't work in enemy subs (except on items that only require the default "idcard" tag)
|
||||
if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
bool shouldBreak = false;
|
||||
bool inEditor = false;
|
||||
#if CLIENT
|
||||
@@ -747,7 +767,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null)
|
||||
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float applyOnUserFraction = 0.0f)
|
||||
{
|
||||
if (statusEffectLists == null) { return; }
|
||||
|
||||
@@ -759,7 +779,13 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; }
|
||||
if (user != null) { effect.SetUser(user); }
|
||||
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, false, false, worldPosition);
|
||||
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
|
||||
if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
effect.AfflictionMultiplier = applyOnUserFraction;
|
||||
item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition);
|
||||
effect.AfflictionMultiplier = 1.0f;
|
||||
}
|
||||
reducesCondition |= effect.ReducesItemCondition();
|
||||
}
|
||||
//if any of the effects reduce the item's condition, set the user for OnBroken effects as well
|
||||
@@ -939,7 +965,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public void ParseMsg()
|
||||
public virtual void ParseMsg()
|
||||
{
|
||||
string msg = TextManager.Get(Msg, true);
|
||||
if (msg != null)
|
||||
@@ -962,7 +988,7 @@ namespace Barotrauma.Items.Components
|
||||
AIObjectiveContainItem containObjective = null;
|
||||
if (character.AIController is HumanAIController aiController)
|
||||
{
|
||||
containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound)
|
||||
containObjective = new AIObjectiveContainItem(character, container.ContainableItemIdentifiers.ToArray(), container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound)
|
||||
{
|
||||
targetItemCount = itemCount,
|
||||
Equip = equip,
|
||||
|
||||
@@ -5,6 +5,8 @@ using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using FarseerPhysics;
|
||||
using System.Collections.Immutable;
|
||||
using Barotrauma.Abilities;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
@@ -23,6 +25,28 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
class SlotRestrictions
|
||||
{
|
||||
public readonly int MaxStackSize;
|
||||
public readonly List<RelatedItem> ContainableItems;
|
||||
|
||||
public SlotRestrictions(int maxStackSize, List<RelatedItem> containableItems)
|
||||
{
|
||||
MaxStackSize = maxStackSize;
|
||||
ContainableItems = containableItems;
|
||||
}
|
||||
|
||||
public bool MatchesItem(Item item)
|
||||
{
|
||||
return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(item));
|
||||
}
|
||||
|
||||
public bool MatchesItem(ItemPrefab itemPrefab)
|
||||
{
|
||||
return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(itemPrefab));
|
||||
}
|
||||
}
|
||||
|
||||
private bool alwaysContainedItemsSpawned;
|
||||
|
||||
public ItemInventory Inventory;
|
||||
@@ -37,7 +61,7 @@ namespace Barotrauma.Items.Components
|
||||
public int Capacity
|
||||
{
|
||||
get { return capacity; }
|
||||
set { capacity = Math.Max(value, 1); }
|
||||
set { capacity = Math.Max(value, 0); }
|
||||
}
|
||||
|
||||
//how many items can be contained
|
||||
@@ -62,17 +86,12 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
[Editable]
|
||||
#endif
|
||||
[Serialize("0.0,0.0", false, description: "The position where the contained items get drawn at (offset from the upper left corner of the sprite in pixels).")]
|
||||
public Vector2 ItemPos { get; set; }
|
||||
|
||||
#if DEBUG
|
||||
[Editable]
|
||||
#endif
|
||||
[Serialize("0.0,0.0", false, description: "The interval at which the contained items are spaced apart from each other (in pixels).")]
|
||||
public Vector2 ItemInterval { get; set; }
|
||||
|
||||
[Serialize(100, false, description: "How many items are placed in a row before starting a new row.")]
|
||||
public int ItemsPerRow { get; set; }
|
||||
|
||||
@@ -90,6 +109,12 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, false)]
|
||||
public bool AllowSwappingContainedItems
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")]
|
||||
public bool AutoInteractWithContained
|
||||
@@ -98,6 +123,9 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, false)]
|
||||
public bool AllowAccess { get; set; }
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool AccessOnlyWhenBroken { get; set; }
|
||||
|
||||
@@ -140,13 +168,29 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Should the items be injected into the user.")]
|
||||
public bool AutoInject
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.5f, false, description: "The health threshold that the user must reach in order to activate the autoinjection.")]
|
||||
public float AutoInjectThreshold
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool RemoveContainedItemsOnDeconstruct { get; set; }
|
||||
|
||||
private SlotRestrictions[] slotRestrictions;
|
||||
|
||||
public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
|
||||
{
|
||||
isRestrictionsDefined = containableRestrictions.Any();
|
||||
if (ContainableItems.None(ri => ri.MatchesItem(item))) { return false; }
|
||||
if (slotRestrictions.None(s => s.MatchesItem(item))) { return false; }
|
||||
if (!isRestrictionsDefined) { return true; }
|
||||
return identifiersOrTags.Any(id => containableRestrictions.Any(r => r == id));
|
||||
}
|
||||
@@ -154,22 +198,22 @@ namespace Barotrauma.Items.Components
|
||||
public bool ShouldBeContained(Item item, out bool isRestrictionsDefined)
|
||||
{
|
||||
isRestrictionsDefined = containableRestrictions.Any();
|
||||
if (ContainableItems.None(ri => ri.MatchesItem(item))) { return false; }
|
||||
if (slotRestrictions.None(s => s.MatchesItem(item))) { return false; }
|
||||
if (!isRestrictionsDefined) { return true; }
|
||||
return containableRestrictions.Any(id => item.Prefab.Identifier == id || item.HasTag(id));
|
||||
}
|
||||
|
||||
public List<RelatedItem> ContainableItems { get; private set; } = new List<RelatedItem>();
|
||||
|
||||
public IEnumerable<string> GetContainableItemIdentifiers => ContainableItems.SelectMany(ri => ri.Identifiers);
|
||||
private ImmutableHashSet<string> containableItemIdentifiers;
|
||||
public IEnumerable<string> ContainableItemIdentifiers => containableItemIdentifiers;
|
||||
|
||||
public override bool RecreateGUIOnResolutionChange => true;
|
||||
|
||||
public ItemContainer(Item item, XElement element)
|
||||
: base (item, element)
|
||||
: base(item, element)
|
||||
{
|
||||
Inventory = new ItemInventory(item, this, capacity, SlotsPerRow);
|
||||
|
||||
int totalCapacity = capacity;
|
||||
|
||||
List<RelatedItem> containableItems = null;
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
@@ -181,34 +225,95 @@ namespace Barotrauma.Items.Components
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers.");
|
||||
continue;
|
||||
}
|
||||
ContainableItems.Add(containable);
|
||||
containableItems ??= new List<RelatedItem>();
|
||||
containableItems.Add(containable);
|
||||
break;
|
||||
case "subcontainer":
|
||||
totalCapacity += subElement.GetAttributeInt("capacity", 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow);
|
||||
slotRestrictions = new SlotRestrictions[totalCapacity];
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
slotRestrictions[i] = new SlotRestrictions(maxStackSize, containableItems);
|
||||
}
|
||||
|
||||
int subContainerIndex = capacity;
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().ToLowerInvariant() != "subcontainer") { continue; }
|
||||
|
||||
int subCapacity = subElement.GetAttributeInt("capacity", 1);
|
||||
int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize);
|
||||
|
||||
List<RelatedItem> subContainableItems = null;
|
||||
foreach (XElement subSubElement in subElement.Elements())
|
||||
{
|
||||
if (subSubElement.Name.ToString().ToLowerInvariant() != "containable") { continue; }
|
||||
|
||||
RelatedItem containable = RelatedItem.Load(subSubElement, returnEmpty: false, parentDebugName: item.Name);
|
||||
if (containable == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers.");
|
||||
continue;
|
||||
}
|
||||
subContainableItems ??= new List<RelatedItem>();
|
||||
subContainableItems.Add(containable);
|
||||
}
|
||||
|
||||
for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++)
|
||||
{
|
||||
slotRestrictions[i] = new SlotRestrictions(subMaxStackSize, subContainableItems);
|
||||
}
|
||||
subContainerIndex += subCapacity;
|
||||
}
|
||||
capacity = totalCapacity;
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
public int GetMaxStackSize(int slotIndex)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= capacity)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return slotRestrictions[slotIndex].MaxStackSize;
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public void OnItemContained(Item containedItem)
|
||||
{
|
||||
item.SetContainedItemPositions();
|
||||
|
||||
RelatedItem ri = ContainableItems.Find(x => x.MatchesItem(containedItem));
|
||||
if (ri != null)
|
||||
|
||||
int index = Inventory.FindIndex(containedItem);
|
||||
if (index >= 0 && index < slotRestrictions.Length)
|
||||
{
|
||||
activeContainedItems.RemoveAll(i => i.Item == containedItem);
|
||||
foreach (StatusEffect effect in ri.statusEffects)
|
||||
if (slotRestrictions[index].ContainableItems != null)
|
||||
{
|
||||
activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, ri.ExcludeBroken));
|
||||
activeContainedItems.RemoveAll(i => i.Item == containedItem);
|
||||
foreach (var containableItem in slotRestrictions[index].ContainableItems)
|
||||
{
|
||||
if (!containableItem.MatchesItem(containedItem)) { continue; }
|
||||
foreach (StatusEffect effect in containableItem.statusEffects)
|
||||
{
|
||||
activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//no need to Update() if this item has no statuseffects and no physics body
|
||||
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
|
||||
}
|
||||
|
||||
public override void Move(Vector2 amount)
|
||||
{
|
||||
SetContainedItemPositions();
|
||||
}
|
||||
|
||||
public void OnItemRemoved(Item containedItem)
|
||||
{
|
||||
activeContainedItems.RemoveAll(i => i.Item == containedItem);
|
||||
@@ -219,13 +324,24 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public bool CanBeContained(Item item)
|
||||
{
|
||||
if (ContainableItems.Count == 0) { return true; }
|
||||
return ContainableItems.Find(c => c.MatchesItem(item)) != null;
|
||||
return slotRestrictions.Any(s => s.MatchesItem(item));
|
||||
}
|
||||
|
||||
public bool CanBeContained(Item item, int index)
|
||||
{
|
||||
if (index < 0 || index >= capacity) { return false; }
|
||||
return slotRestrictions[index].MatchesItem(item);
|
||||
}
|
||||
|
||||
public bool CanBeContained(ItemPrefab itemPrefab)
|
||||
{
|
||||
if (ContainableItems.Count == 0) { return true; }
|
||||
return ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null;
|
||||
return slotRestrictions.Any(s => s.MatchesItem(itemPrefab));
|
||||
}
|
||||
|
||||
public bool CanBeContained(ItemPrefab itemPrefab, int index)
|
||||
{
|
||||
if (index < 0 || index >= capacity) { return false; }
|
||||
return slotRestrictions[index].MatchesItem(itemPrefab);
|
||||
}
|
||||
|
||||
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
||||
@@ -237,9 +353,24 @@ namespace Barotrauma.Items.Components
|
||||
SpawnAlwaysContainedItems();
|
||||
}
|
||||
|
||||
if (item.ParentInventory is CharacterInventory)
|
||||
if (item.ParentInventory is CharacterInventory ownerInventory)
|
||||
{
|
||||
item.SetContainedItemPositions();
|
||||
|
||||
if (AutoInject)
|
||||
{
|
||||
if (ownerInventory?.Owner is Character ownerCharacter &&
|
||||
ownerCharacter.HealthPercentage / 100f <= AutoInjectThreshold &&
|
||||
ownerCharacter.HasEquippedItem(item))
|
||||
{
|
||||
foreach (Item item in Inventory.AllItemsMod)
|
||||
{
|
||||
item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter);
|
||||
item.GetComponent<GeneticMaterial>()?.Equip(ownerCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else if (item.body != null &&
|
||||
item.body.Enabled &&
|
||||
@@ -256,6 +387,7 @@ namespace Barotrauma.Items.Components
|
||||
foreach (var activeContainedItem in activeContainedItems)
|
||||
{
|
||||
Item contained = activeContainedItem.Item;
|
||||
|
||||
if (activeContainedItem.ExcludeBroken && contained.Condition <= 0.0f) { continue; }
|
||||
StatusEffect effect = activeContainedItem.StatusEffect;
|
||||
|
||||
@@ -275,11 +407,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool HasRequiredItems(Character character, bool addMessage, string msg = null)
|
||||
{
|
||||
return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
|
||||
return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
|
||||
}
|
||||
|
||||
public override bool Select(Character character)
|
||||
{
|
||||
if (!AllowAccess) { return false; }
|
||||
if (item.Container != null) { return false; }
|
||||
if (AccessOnlyWhenBroken)
|
||||
{
|
||||
@@ -299,11 +432,15 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
var abilityItem = new AbilityItem(item);
|
||||
character.CheckTalents(AbilityEffectType.OnOpenItemContainer, abilityItem);
|
||||
|
||||
return base.Select(character);
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
if (!AllowAccess) { return false; }
|
||||
if (AccessOnlyWhenBroken)
|
||||
{
|
||||
if (item.Condition > 0)
|
||||
@@ -331,7 +468,7 @@ namespace Barotrauma.Items.Components
|
||||
public override bool Combine(Item item, Character user)
|
||||
{
|
||||
if (!AllowDragAndDrop && user != null) { return false; }
|
||||
if (!ContainableItems.Any(it => it.MatchesItem(item))) { return false; }
|
||||
if (!slotRestrictions.Any(s => s.MatchesItem(item))) { return false; }
|
||||
if (user != null && !user.CanAccessInventory(Inventory)) { return false; }
|
||||
|
||||
if (Inventory.TryPutItem(item, user))
|
||||
@@ -361,52 +498,65 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 transformedItemInterval = ItemInterval * item.Scale;
|
||||
Vector2 transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f);
|
||||
Vector2 transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y);
|
||||
if (item.body == null)
|
||||
|
||||
if (ItemPos == Vector2.Zero && ItemInterval == Vector2.Zero)
|
||||
{
|
||||
if (item.FlippedX)
|
||||
{
|
||||
transformedItemPos.X = -transformedItemPos.X;
|
||||
transformedItemPos.X += item.Rect.Width;
|
||||
transformedItemInterval.X = -transformedItemInterval.X;
|
||||
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
|
||||
}
|
||||
if (item.FlippedY)
|
||||
{
|
||||
transformedItemPos.Y = -transformedItemPos.Y;
|
||||
transformedItemPos.Y -= item.Rect.Height;
|
||||
transformedItemInterval.Y = -transformedItemInterval.Y;
|
||||
transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y;
|
||||
}
|
||||
transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y);
|
||||
if (Math.Abs(item.Rotation) > 0.01f)
|
||||
{
|
||||
Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation));
|
||||
transformedItemPos = Vector2.Transform(transformedItemPos, transform);
|
||||
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
|
||||
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
|
||||
transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
|
||||
}
|
||||
transformedItemPos = item.Position;
|
||||
}
|
||||
else
|
||||
{
|
||||
Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
|
||||
if (item.body.Dir == -1.0f)
|
||||
if (item.body == null)
|
||||
{
|
||||
transformedItemPos.X = -transformedItemPos.X;
|
||||
transformedItemInterval.X = -transformedItemInterval.X;
|
||||
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
|
||||
if (item.FlippedX)
|
||||
{
|
||||
transformedItemPos.X = -transformedItemPos.X;
|
||||
transformedItemPos.X += item.Rect.Width;
|
||||
transformedItemInterval.X = -transformedItemInterval.X;
|
||||
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
|
||||
}
|
||||
if (item.FlippedY)
|
||||
{
|
||||
transformedItemPos.Y = -transformedItemPos.Y;
|
||||
transformedItemPos.Y -= item.Rect.Height;
|
||||
transformedItemInterval.Y = -transformedItemInterval.Y;
|
||||
transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y;
|
||||
}
|
||||
transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y);
|
||||
if (Math.Abs(item.Rotation) > 0.01f)
|
||||
{
|
||||
Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation));
|
||||
transformedItemPos = Vector2.Transform(transformedItemPos - item.Position, transform) + item.Position;
|
||||
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
|
||||
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
|
||||
transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
|
||||
}
|
||||
}
|
||||
transformedItemPos = Vector2.Transform(transformedItemPos, transform);
|
||||
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
|
||||
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
|
||||
transformedItemPos += item.Position;
|
||||
}
|
||||
else
|
||||
{
|
||||
Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
|
||||
if (item.body.Dir == -1.0f)
|
||||
{
|
||||
transformedItemPos.X = -transformedItemPos.X;
|
||||
transformedItemInterval.X = -transformedItemInterval.X;
|
||||
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
|
||||
}
|
||||
transformedItemPos = Vector2.Transform(transformedItemPos, transform);
|
||||
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
|
||||
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
|
||||
transformedItemPos += item.Position;
|
||||
}
|
||||
}
|
||||
|
||||
float currentRotation = itemRotation;
|
||||
if (item.body != null)
|
||||
{
|
||||
currentRotation *= item.body.Dir;
|
||||
currentRotation += item.body.Rotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentRotation += MathHelper.ToRadians(-item.Rotation);
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
Vector2 currentItemPos = transformedItemPos;
|
||||
@@ -461,6 +611,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
Inventory.AllowSwappingContainedItems = AllowSwappingContainedItems;
|
||||
containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty<string>()).ToImmutableHashSet();
|
||||
if (item.Submarine == null || !item.Submarine.Loading)
|
||||
{
|
||||
SpawnAlwaysContainedItems();
|
||||
@@ -488,7 +640,21 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
itemIds = null;
|
||||
}
|
||||
SpawnAlwaysContainedItems();
|
||||
|
||||
//outpost and ruins are loaded in multiple stages (each module is loaded separately)
|
||||
//spawning items at this point during the generation will cause ID overlaps with the entities in the modules loaded afterwards
|
||||
//so let's not spawn them at this point, but in the 1st Update()
|
||||
if (item.Submarine?.Info != null && (item.Submarine.Info.IsOutpost || item.Submarine.Info.IsRuin))
|
||||
{
|
||||
if (SpawnWithId.Length > 0)
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SpawnAlwaysContainedItems();
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnAlwaysContainedItems()
|
||||
|
||||
+232
-79
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -14,6 +15,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool hasPower;
|
||||
|
||||
private Character user;
|
||||
|
||||
private float userDeconstructorSpeedMultiplier = 1.0f;
|
||||
|
||||
private const float TinkeringSpeedIncrease = 1.5f;
|
||||
|
||||
private ItemContainer inputContainer, outputContainer;
|
||||
|
||||
public ItemContainer InputContainer
|
||||
@@ -25,7 +32,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
get { return outputContainer; }
|
||||
}
|
||||
|
||||
|
||||
[Serialize(false, true)]
|
||||
public bool DeconstructItemsSimultaneously { get; set; }
|
||||
|
||||
[Editable, Serialize(1.0f, true)]
|
||||
public float DeconstructionSpeed { get; set; }
|
||||
|
||||
@@ -81,65 +91,177 @@ namespace Barotrauma.Items.Components
|
||||
if (powerConsumption <= 0.0f) { Voltage = 1.0f; }
|
||||
progressTimer += deltaTime * Math.Min(Voltage, 1.0f);
|
||||
|
||||
var targetItem = inputContainer.Inventory.LastOrDefault();
|
||||
if (targetItem == null) { return; }
|
||||
|
||||
float deconstructTime = targetItem.Prefab.DeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / DeconstructionSpeed : 1.0f;
|
||||
|
||||
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
|
||||
if (progressTimer > deconstructTime)
|
||||
float tinkeringStrength = 0f;
|
||||
if (repairable.IsTinkering)
|
||||
{
|
||||
// In multiplayer, the server handles the deconstruction into new items
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
tinkeringStrength = repairable.TinkeringStrength;
|
||||
}
|
||||
// doesn't quite work properly, remaining time changes if tinkering stops
|
||||
float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease);
|
||||
|
||||
if (targetItem.Prefab.RandomDeconstructionOutput)
|
||||
if (DeconstructItemsSimultaneously)
|
||||
{
|
||||
float deconstructTime = 0.0f;
|
||||
foreach (Item targetItem in inputContainer.Inventory.AllItems)
|
||||
{
|
||||
int amount = targetItem.Prefab.RandomDeconstructionOutputAmount;
|
||||
List<int> deconstructItemIndexes = new List<int>();
|
||||
for (int i = 0; i < targetItem.Prefab.DeconstructItems.Count; i++)
|
||||
{
|
||||
deconstructItemIndexes.Add(i);
|
||||
}
|
||||
List<float> commonness = targetItem.Prefab.DeconstructItems.Select(i => i.Commonness).ToList();
|
||||
List<DeconstructItem> products = new List<DeconstructItem>();
|
||||
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
if (deconstructItemIndexes.Count < 1) { break; }
|
||||
var itemIndex = ToolBox.SelectWeightedRandom(deconstructItemIndexes, commonness, Rand.RandSync.Unsynced);
|
||||
products.Add(targetItem.Prefab.DeconstructItems[itemIndex]);
|
||||
var removeIndex = deconstructItemIndexes.IndexOf(itemIndex);
|
||||
deconstructItemIndexes.RemoveAt(removeIndex);
|
||||
commonness.RemoveAt(removeIndex);
|
||||
}
|
||||
foreach (DeconstructItem deconstructProduct in products)
|
||||
{
|
||||
CreateDeconstructProduct(deconstructProduct);
|
||||
}
|
||||
deconstructTime += targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier);
|
||||
}
|
||||
else
|
||||
|
||||
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
|
||||
if (progressTimer > deconstructTime)
|
||||
{
|
||||
foreach (DeconstructItem deconstructProduct in targetItem.Prefab.DeconstructItems)
|
||||
List<Item> items = inputContainer.Inventory.AllItems.ToList();
|
||||
foreach (Item targetItem in items)
|
||||
{
|
||||
CreateDeconstructProduct(deconstructProduct);
|
||||
if ((Entity.Spawner?.IsInRemoveQueue(targetItem) ?? false) || !inputContainer.Inventory.AllItems.Contains(targetItem)) { continue; }
|
||||
var validDeconstructItems = targetItem.Prefab.DeconstructItems.FindAll(it =>
|
||||
(it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) &&
|
||||
(it.RequiredOtherItem.Length == 0 || it.RequiredOtherItem.Any(r => items.Any(it => it != targetItem && (it.HasTag(r) || it.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))))));
|
||||
|
||||
ProcessItem(targetItem, items, validDeconstructItems, allowRemove: validDeconstructItems.Any() || !targetItem.Prefab.DeconstructItems.Any());
|
||||
}
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
progressTimer = 0.0f;
|
||||
progressState = 0.0f;
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var targetItem = inputContainer.Inventory.LastOrDefault();
|
||||
if (targetItem == null) { return; }
|
||||
|
||||
var validDeconstructItems = targetItem.Prefab.DeconstructItems.FindAll(it =>
|
||||
it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier) : 1.0f;
|
||||
|
||||
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
|
||||
if (progressTimer > deconstructTime)
|
||||
{
|
||||
ProcessItem(targetItem, inputContainer.Inventory.AllItemsMod, validDeconstructItems, allowRemove: validDeconstructItems.Any() || !targetItem.Prefab.DeconstructItems.Any());
|
||||
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
progressTimer = 0.0f;
|
||||
progressState = 0.0f;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessItem(Item targetItem, IEnumerable<Item> inputItems, List<DeconstructItem> validDeconstructItems, bool allowRemove = true)
|
||||
{
|
||||
// In multiplayer, the server handles the deconstruction into new items
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
if (user != null && !user.Removed)
|
||||
{
|
||||
var abilityTargetItem = new AbilityItem(targetItem);
|
||||
user.CheckTalents(AbilityEffectType.OnItemDeconstructed, abilityTargetItem);
|
||||
}
|
||||
|
||||
if (targetItem.Prefab.RandomDeconstructionOutput)
|
||||
{
|
||||
int amount = targetItem.Prefab.RandomDeconstructionOutputAmount;
|
||||
List<int> deconstructItemIndexes = new List<int>();
|
||||
for (int i = 0; i < validDeconstructItems.Count; i++)
|
||||
{
|
||||
deconstructItemIndexes.Add(i);
|
||||
}
|
||||
List<float> commonness = validDeconstructItems.Select(i => i.Commonness).ToList();
|
||||
List<DeconstructItem> products = new List<DeconstructItem>();
|
||||
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
if (deconstructItemIndexes.Count < 1) { break; }
|
||||
var itemIndex = ToolBox.SelectWeightedRandom(deconstructItemIndexes, commonness, Rand.RandSync.Unsynced);
|
||||
products.Add(validDeconstructItems[itemIndex]);
|
||||
var removeIndex = deconstructItemIndexes.IndexOf(itemIndex);
|
||||
deconstructItemIndexes.RemoveAt(removeIndex);
|
||||
commonness.RemoveAt(removeIndex);
|
||||
}
|
||||
|
||||
foreach (DeconstructItem deconstructProduct in products)
|
||||
{
|
||||
CreateDeconstructProduct(deconstructProduct, inputItems);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (DeconstructItem deconstructProduct in validDeconstructItems)
|
||||
{
|
||||
CreateDeconstructProduct(deconstructProduct, inputItems);
|
||||
}
|
||||
}
|
||||
|
||||
void CreateDeconstructProduct(DeconstructItem deconstructProduct, IEnumerable<Item> inputItems)
|
||||
{
|
||||
float percentageHealth = targetItem.Condition / targetItem.MaxCondition;
|
||||
|
||||
if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; }
|
||||
|
||||
if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab))
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!");
|
||||
return;
|
||||
}
|
||||
|
||||
float condition = deconstructProduct.CopyCondition ?
|
||||
percentageHealth * itemPrefab.Health :
|
||||
itemPrefab.Health * Rand.Range(deconstructProduct.OutConditionMin, deconstructProduct.OutConditionMax);
|
||||
|
||||
if (DeconstructItemsSimultaneously && deconstructProduct.RequiredOtherItem.Length > 0)
|
||||
{
|
||||
foreach (Item otherItem in inputItems)
|
||||
{
|
||||
if (targetItem == otherItem) { continue; }
|
||||
if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r.Equals(otherItem.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
user.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined);
|
||||
foreach (Character character in Character.GetFriendlyCrew(user))
|
||||
{
|
||||
character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined);
|
||||
}
|
||||
|
||||
var geneticMaterial1 = targetItem.GetComponent<GeneticMaterial>();
|
||||
var geneticMaterial2 = otherItem.GetComponent<GeneticMaterial>();
|
||||
if (geneticMaterial1 != null && geneticMaterial2 != null)
|
||||
{
|
||||
if (geneticMaterial1.Combine(geneticMaterial2, user))
|
||||
{
|
||||
inputContainer.Inventory.RemoveItem(otherItem);
|
||||
OutputContainer.Inventory.RemoveItem(otherItem);
|
||||
Entity.Spawner.AddToRemoveQueue(otherItem);
|
||||
}
|
||||
allowRemove = false;
|
||||
return;
|
||||
}
|
||||
inputContainer.Inventory.RemoveItem(otherItem);
|
||||
OutputContainer.Inventory.RemoveItem(otherItem);
|
||||
Entity.Spawner.AddToRemoveQueue(otherItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CreateDeconstructProduct(DeconstructItem deconstructProduct)
|
||||
int amount = 1;
|
||||
|
||||
if (user != null && !user.Removed)
|
||||
{
|
||||
float percentageHealth = targetItem.Condition / targetItem.Prefab.Health;
|
||||
if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; }
|
||||
var itemsCreated = new AbilityValueItem(amount, targetItem.Prefab);
|
||||
user.CheckTalents(AbilityEffectType.OnItemDeconstructedMaterial, itemsCreated);
|
||||
amount = (int)itemsCreated.Value;
|
||||
|
||||
if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab))
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!");
|
||||
return;
|
||||
}
|
||||
|
||||
float condition = deconstructProduct.CopyCondition ?
|
||||
percentageHealth * itemPrefab.Health :
|
||||
itemPrefab.Health * deconstructProduct.OutCondition;
|
||||
// used to spawn items directly into the deconstructor
|
||||
var itemContainer = new AbilityItemPrefabItem(item, targetItem.Prefab);
|
||||
user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemContainer);
|
||||
}
|
||||
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) =>
|
||||
{
|
||||
for (int i = 0; i < outputContainer.Capacity; i++)
|
||||
@@ -153,36 +275,31 @@ namespace Barotrauma.Items.Components
|
||||
PutItemsToLinkedContainer();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (targetItem.Prefab.AllowDeconstruct)
|
||||
if (targetItem.AllowDeconstruct && allowRemove)
|
||||
{
|
||||
//drop all items that are inside the deconstructed item
|
||||
foreach (ItemContainer ic in targetItem.GetComponents<ItemContainer>())
|
||||
{
|
||||
//drop all items that are inside the deconstructed item
|
||||
foreach (ItemContainer ic in targetItem.GetComponents<ItemContainer>())
|
||||
{
|
||||
if (ic?.Inventory == null || ic.RemoveContainedItemsOnDeconstruct) { continue; }
|
||||
ic.Inventory.AllItemsMod.ForEach(containedItem => outputContainer.Inventory.TryPutItem(containedItem, user: null));
|
||||
}
|
||||
inputContainer.Inventory.RemoveItem(targetItem);
|
||||
Entity.Spawner.AddToRemoveQueue(targetItem);
|
||||
MoveInputQueue();
|
||||
PutItemsToLinkedContainer();
|
||||
if (ic?.Inventory == null || ic.RemoveContainedItemsOnDeconstruct) { continue; }
|
||||
ic.Inventory.AllItemsMod.ForEach(containedItem => outputContainer.Inventory.TryPutItem(containedItem, user: null));
|
||||
}
|
||||
inputContainer.Inventory.RemoveItem(targetItem);
|
||||
Entity.Spawner.AddToRemoveQueue(targetItem);
|
||||
MoveInputQueue();
|
||||
PutItemsToLinkedContainer();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!outputContainer.Inventory.CanBePut(targetItem) || (Entity.Spawner?.IsInRemoveQueue(targetItem) ?? false))
|
||||
{
|
||||
targetItem.Drop(dropper: null);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!outputContainer.Inventory.CanBePut(targetItem))
|
||||
{
|
||||
targetItem.Drop(dropper: null);
|
||||
}
|
||||
else
|
||||
{
|
||||
outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true);
|
||||
}
|
||||
outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true);
|
||||
}
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
progressTimer = 0.0f;
|
||||
progressState = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +307,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
if (outputContainer.Inventory.IsEmpty()) { return; }
|
||||
|
||||
|
||||
foreach (MapEntity linkedTo in item.linkedTo)
|
||||
{
|
||||
if (linkedTo is Item linkedItem)
|
||||
@@ -201,7 +318,7 @@ namespace Barotrauma.Items.Components
|
||||
if (itemContainer == null) { continue; }
|
||||
outputContainer.Inventory.AllItemsMod.ForEach(containedItem => itemContainer.Inventory.TryPutItem(containedItem, user: null, createNetworkEvent: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -221,14 +338,54 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<(Item item, DeconstructItem output)> GetAvailableOutputs(bool checkRequiredOtherItems = true)
|
||||
{
|
||||
var items = inputContainer.Inventory.AllItems;
|
||||
foreach (Item inputItem in items)
|
||||
{
|
||||
if (!inputItem.AllowDeconstruct) { continue; }
|
||||
foreach (var deconstructItem in inputItem.Prefab.DeconstructItems)
|
||||
{
|
||||
if (deconstructItem.RequiredDeconstructor.Length > 0)
|
||||
{
|
||||
if (!deconstructItem.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) { continue; }
|
||||
}
|
||||
if (deconstructItem.RequiredOtherItem.Length > 0 && checkRequiredOtherItems)
|
||||
{
|
||||
if (!deconstructItem.RequiredOtherItem.Any(r => items.Any(it => it.HasTag(r) || it.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase)))) { continue; }
|
||||
bool validOtherItemFound = false;
|
||||
foreach (Item otherInputItem in items)
|
||||
{
|
||||
if (otherInputItem == inputItem) { continue; }
|
||||
if (!deconstructItem.RequiredOtherItem.Any(r => otherInputItem.HasTag(r) || otherInputItem.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) { continue; }
|
||||
|
||||
var geneticMaterial1 = inputItem.GetComponent<GeneticMaterial>();
|
||||
var geneticMaterial2 = otherInputItem.GetComponent<GeneticMaterial>();
|
||||
if (geneticMaterial1 != null && geneticMaterial2 != null)
|
||||
{
|
||||
if (!geneticMaterial1.CanBeCombinedWith(geneticMaterial2)) { continue; }
|
||||
}
|
||||
validOtherItemFound = true;
|
||||
}
|
||||
if (!validOtherItemFound) { continue; }
|
||||
}
|
||||
yield return (inputItem, deconstructItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetActive(bool active, Character user = null)
|
||||
{
|
||||
PutItemsToLinkedContainer();
|
||||
|
||||
this.user = user;
|
||||
|
||||
if (inputContainer.Inventory.IsEmpty()) { active = false; }
|
||||
|
||||
IsActive = active;
|
||||
currPowerConsumption = IsActive ? powerConsumption : 0.0f;
|
||||
userDeconstructorSpeedMultiplier = user != null ? 1f + user.GetStatValue(StatTypes.DeconstructorSpeedMultiplier) : 1f;
|
||||
|
||||
#if SERVER
|
||||
if (user != null)
|
||||
{
|
||||
@@ -241,10 +398,6 @@ namespace Barotrauma.Items.Components
|
||||
progressState = 0.0f;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
activateButton.Text = TextManager.Get(IsActive ? "DeconstructorCancel" : "DeconstructorDeconstruct");
|
||||
#endif
|
||||
|
||||
inputContainer.Inventory.Locked = IsActive;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private const float TinkeringForceIncrease = 1.5f;
|
||||
|
||||
public Engine(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -113,31 +115,35 @@ namespace Barotrauma.Items.Components
|
||||
Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, 0.1f);
|
||||
if (Math.Abs(Force) > 1.0f)
|
||||
{
|
||||
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f);
|
||||
float currForce = force * voltageFactor;
|
||||
float condition = item.Condition / item.MaxCondition;
|
||||
// Broken engine makes more noise.
|
||||
float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition);
|
||||
UpdateAITargets(noise);
|
||||
//arbitrary multiplier that was added to changes in submarine mass without having to readjust all engines
|
||||
float forceMultiplier = 0.1f;
|
||||
if (User != null)
|
||||
{
|
||||
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
|
||||
}
|
||||
currForce *= maxForce * forceMultiplier;
|
||||
if (item.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering)
|
||||
{
|
||||
currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease;
|
||||
}
|
||||
|
||||
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f);
|
||||
Vector2 currForce = new Vector2(force * maxForce * forceMultiplier * voltageFactor, 0.0f);
|
||||
//less effective when in a bad condition
|
||||
currForce *= MathHelper.Lerp(0.5f, 2.0f, item.Condition / item.MaxCondition);
|
||||
currForce *= MathHelper.Lerp(0.5f, 2.0f, condition);
|
||||
if (item.Submarine.FlippedX) { currForce *= -1; }
|
||||
item.Submarine.ApplyForce(currForce);
|
||||
Vector2 forceVector = new Vector2(currForce, 0);
|
||||
item.Submarine.ApplyForce(forceVector);
|
||||
UpdatePropellerDamage(deltaTime);
|
||||
float maxChangeSpeed = 0.5f;
|
||||
float modifier = 2;
|
||||
float noise = MathUtils.NearlyEqual(0.0f, maxForce) ? 0.0f : currForce.Length() * forceMultiplier * modifier / maxForce;
|
||||
float min = Math.Max(1 - maxChangeSpeed, 0);
|
||||
float max = 1 + maxChangeSpeed;
|
||||
UpdateAITargets(Math.Clamp(noise, min, max), deltaTime);
|
||||
#if CLIENT
|
||||
particleTimer -= deltaTime;
|
||||
if (particleTimer <= 0.0f)
|
||||
{
|
||||
Vector2 particleVel = -currForce.ClampLength(5000.0f) / 5.0f;
|
||||
Vector2 particleVel = -forceVector.ClampLength(5000.0f) / 5.0f;
|
||||
GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos * item.Scale,
|
||||
particleVel * Rand.Range(0.9f, 1.1f),
|
||||
0.0f, item.CurrentHull);
|
||||
@@ -147,14 +153,14 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAITargets(float increaseSpeed, float deltaTime)
|
||||
private void UpdateAITargets(float noise)
|
||||
{
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
item.AiTarget.IncreaseSoundRange(deltaTime, increaseSpeed);
|
||||
item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, noise / 100);
|
||||
if (item.CurrentHull != null && item.CurrentHull.AiTarget != null)
|
||||
{
|
||||
// It's possible that some othe item increases the hull's soundrange more than the engine.
|
||||
// It's possible that some other item increases the hull's soundrange more than the engine.
|
||||
item.CurrentHull.AiTarget.SoundRange = Math.Max(item.CurrentHull.AiTarget.SoundRange, item.AiTarget.SoundRange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Abilities;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
@@ -32,6 +32,8 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(1.0f, true)]
|
||||
public float SkillRequirementMultiplier { get; set; }
|
||||
|
||||
private const float TinkeringSpeedIncrease = 1.5f;
|
||||
|
||||
private enum FabricatorState
|
||||
{
|
||||
Active = 1,
|
||||
@@ -240,7 +242,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (fabricatedItem == null || !CanBeFabricated(fabricatedItem))
|
||||
var availableIngredients = GetAvailableIngredients();
|
||||
if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user))
|
||||
{
|
||||
CancelFabricating();
|
||||
return;
|
||||
@@ -278,48 +281,91 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (powerConsumption <= 0) { Voltage = 1.0f; }
|
||||
|
||||
timeUntilReady -= deltaTime * Math.Min(Voltage, 1.0f);
|
||||
float tinkeringStrength = 0f;
|
||||
if (repairable.IsTinkering)
|
||||
{
|
||||
tinkeringStrength = repairable.TinkeringStrength;
|
||||
}
|
||||
float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease;
|
||||
|
||||
timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(Voltage, 1.0f);
|
||||
|
||||
UpdateRequiredTimeProjSpecific();
|
||||
|
||||
if (timeUntilReady > 0.0f) { return; }
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
var availableIngredients = GetAvailableIngredients();
|
||||
foreach (FabricationRecipe.RequiredItem ingredient in fabricatedItem.RequiredItems)
|
||||
{
|
||||
for (int i = 0; i < ingredient.Amount; i++)
|
||||
fabricatedItem.RequiredItems.ForEach(requiredItem => {
|
||||
for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++)
|
||||
{
|
||||
var availableItem = availableIngredients.FirstOrDefault(it =>
|
||||
it != null && ingredient.ItemPrefabs.Contains(it.Prefab) &&
|
||||
it.ConditionPercentage >= ingredient.MinCondition * 100.0f &&
|
||||
it.ConditionPercentage <= ingredient.MaxCondition * 100.0f);
|
||||
if (availableItem == null) { continue; }
|
||||
|
||||
if (ingredient.UseCondition && availableItem.ConditionPercentage - ingredient.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0
|
||||
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
|
||||
{
|
||||
availableItem.Condition -= availableItem.Prefab.Health * ingredient.MinCondition;
|
||||
continue;
|
||||
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
|
||||
|
||||
var availablePrefabs = availableIngredients[requiredPrefab.Identifier];
|
||||
var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab =>
|
||||
{
|
||||
return potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f &&
|
||||
potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f;
|
||||
});
|
||||
|
||||
if (availablePrefab == null) { continue; }
|
||||
|
||||
if (requiredItem.UseCondition && availablePrefab.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0
|
||||
{
|
||||
availablePrefab.Condition -= availablePrefab.Prefab.Health * requiredItem.MinCondition;
|
||||
continue;
|
||||
}
|
||||
|
||||
availablePrefabs.Remove(availablePrefab);
|
||||
Entity.Spawner.AddToRemoveQueue(availablePrefab);
|
||||
inputContainer.Inventory.RemoveItem(availablePrefab);
|
||||
}
|
||||
availableIngredients.Remove(availableItem);
|
||||
Entity.Spawner.AddToRemoveQueue(availableItem);
|
||||
inputContainer.Inventory.RemoveItem(availableItem);
|
||||
}
|
||||
});
|
||||
|
||||
int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health);
|
||||
|
||||
var fabricationValueItem = new AbilityValueItem(fabricatedItem.Amount, fabricatedItem.TargetItem);
|
||||
|
||||
int quality = 0;
|
||||
if (user?.Info != null)
|
||||
{
|
||||
foreach (Character character in Character.CharacterList.Where(c => c.TeamID == user.TeamID))
|
||||
{
|
||||
character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationValueItem);
|
||||
}
|
||||
user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationValueItem);
|
||||
|
||||
quality = GetFabricatedItemQuality(fabricatedItem, user);
|
||||
}
|
||||
|
||||
Character tempUser = user;
|
||||
int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health);
|
||||
for (int i = 0; i < fabricatedItem.Amount; i++)
|
||||
var tempUser = user;
|
||||
for (int i = 0; i < (int)fabricationValueItem.Value; i++)
|
||||
{
|
||||
float outCondition = fabricatedItem.OutCondition;
|
||||
if (i < amountFittingContainer)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition,
|
||||
onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); });
|
||||
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition,
|
||||
onSpawned: (Item spawnedItem) =>
|
||||
{
|
||||
onItemSpawned(spawnedItem, tempUser);
|
||||
spawnedItem.Quality = quality;
|
||||
//reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers
|
||||
spawnedItem.Condition = spawnedItem.MaxCondition * outCondition;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition,
|
||||
onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); });
|
||||
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition,
|
||||
onSpawned: (Item spawnedItem) =>
|
||||
{
|
||||
onItemSpawned(spawnedItem, tempUser);
|
||||
spawnedItem.Quality = quality;
|
||||
//reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers
|
||||
spawnedItem.Condition = spawnedItem.MaxCondition * outCondition;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,16 +379,19 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (user?.Info != null && !user.Removed)
|
||||
{
|
||||
foreach (Skill skill in fabricatedItem.RequiredSkills)
|
||||
{
|
||||
float userSkill = user.GetSkillLevel(skill.Identifier);
|
||||
float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f);
|
||||
var addedSkillValue = new AbilityValueString(0f, skill.Identifier);
|
||||
user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue);
|
||||
addedSkill += addedSkillValue.Value;
|
||||
|
||||
user.Info.IncreaseSkillLevel(
|
||||
skill.Identifier,
|
||||
skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f),
|
||||
user.Position + Vector2.UnitY * 150.0f);
|
||||
addedSkill);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,26 +412,57 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
partial void UpdateRequiredTimeProjSpecific();
|
||||
|
||||
private bool CanBeFabricated(FabricationRecipe fabricableItem)
|
||||
private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
|
||||
{
|
||||
if (fabricableItem == null) { return false; }
|
||||
List<Item> availableIngredients = GetAvailableIngredients();
|
||||
return CanBeFabricated(fabricableItem, availableIngredients);
|
||||
if (user == null) { return 0; }
|
||||
if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; }
|
||||
int quality = 0;
|
||||
float floatQuality = 0.0f;
|
||||
foreach (string tag in fabricatedItem.TargetItem.Tags)
|
||||
{
|
||||
floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag);
|
||||
}
|
||||
quality = (int)floatQuality;
|
||||
|
||||
const int MaxCraftingSkill = 100;
|
||||
|
||||
quality += fabricatedItem.RequiredSkills.All(s => user.GetSkillLevel(s.Identifier) >= MaxCraftingSkill) ? 1 : 0;
|
||||
quality += FabricationDegreeOfSuccess(user, fabricatedItem.RequiredSkills) >= 0.5f ? 1 : 0;
|
||||
return quality;
|
||||
}
|
||||
|
||||
private bool CanBeFabricated(FabricationRecipe fabricableItem, IEnumerable<Item> availableIngredients)
|
||||
partial void UpdateRequiredTimeProjSpecific();
|
||||
|
||||
private bool CanBeFabricated(FabricationRecipe fabricableItem, Dictionary<string, List<Item>> availableIngredients, Character character)
|
||||
{
|
||||
if (fabricableItem == null) { return false; }
|
||||
foreach (FabricationRecipe.RequiredItem requiredItem in fabricableItem.RequiredItems)
|
||||
if (fabricableItem == null) { return false; }
|
||||
if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; }
|
||||
|
||||
return fabricableItem.RequiredItems.All(requiredItem =>
|
||||
{
|
||||
if (availableIngredients.Count(it => IsItemValidIngredient(it, requiredItem)) < requiredItem.Amount)
|
||||
int availablePrefabsAmount = 0;
|
||||
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
|
||||
{
|
||||
return false;
|
||||
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
|
||||
|
||||
var availablePrefabs = availableIngredients[requiredPrefab.Identifier];
|
||||
foreach (Item availablePrefab in availablePrefabs)
|
||||
{
|
||||
if (availablePrefab.Condition / availablePrefab.Prefab.Health >= requiredItem.MinCondition &&
|
||||
availablePrefab.Condition / availablePrefab.Prefab.Health <= requiredItem.MaxCondition)
|
||||
{
|
||||
availablePrefabsAmount++;
|
||||
}
|
||||
|
||||
if (availablePrefabsAmount >= requiredItem.Amount)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private float GetRequiredTime(FabricationRecipe fabricableItem, Character user)
|
||||
@@ -416,7 +496,7 @@ namespace Barotrauma.Items.Components
|
||||
/// Get a list of all items available in the input container and linked containers
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private List<Item> GetAvailableIngredients()
|
||||
private Dictionary<string, List<Item>> GetAvailableIngredients()
|
||||
{
|
||||
List<Item> availableIngredients = new List<Item>();
|
||||
availableIngredients.AddRange(inputContainer.Inventory.AllItems);
|
||||
@@ -448,7 +528,19 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
#endif
|
||||
|
||||
return availableIngredients;
|
||||
Dictionary<string, List<Item>> ingredientsDictionary = new Dictionary<string, List<Item>>();
|
||||
for (int i = 0; i < availableIngredients.Count; i++)
|
||||
{
|
||||
var itemIdentifier = availableIngredients[i].prefab.Identifier;
|
||||
if (!ingredientsDictionary.ContainsKey(itemIdentifier))
|
||||
{
|
||||
ingredientsDictionary[itemIdentifier] = new List<Item>(availableIngredients.Count);
|
||||
}
|
||||
|
||||
ingredientsDictionary[itemIdentifier].Add(availableIngredients[i]);
|
||||
}
|
||||
|
||||
return ingredientsDictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -463,40 +555,41 @@ namespace Barotrauma.Items.Components
|
||||
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
|
||||
|
||||
var availableIngredients = GetAvailableIngredients();
|
||||
foreach (var requiredItem in targetItem.RequiredItems)
|
||||
{
|
||||
targetItem.RequiredItems.ForEach(requiredItem => {
|
||||
for (int i = 0; i < requiredItem.Amount; i++)
|
||||
{
|
||||
var matchingItem = availableIngredients.Find(it => !usedItems.Contains(it) && IsItemValidIngredient(it, requiredItem));
|
||||
if (matchingItem == null) { continue; }
|
||||
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
|
||||
{
|
||||
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
|
||||
|
||||
availableIngredients.Remove(matchingItem);
|
||||
|
||||
if (matchingItem.ParentInventory == inputContainer.Inventory)
|
||||
{
|
||||
//already in input container, all good
|
||||
usedItems.Add(matchingItem);
|
||||
}
|
||||
else //in another inventory, we need to move the item
|
||||
{
|
||||
if (!inputContainer.Inventory.CanBePut(matchingItem))
|
||||
var availablePrefabs = availableIngredients[requiredPrefab.Identifier];
|
||||
var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab =>
|
||||
{
|
||||
var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it));
|
||||
unneededItem?.Drop(null, createNetworkEvent: !isClient);
|
||||
}
|
||||
inputContainer.Inventory.TryPutItem(matchingItem, user: null, createNetworkEvent: !isClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return !usedItems.Contains(potentialPrefab) &&
|
||||
potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f &&
|
||||
potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f;
|
||||
});
|
||||
if (availablePrefab == null) { continue; }
|
||||
|
||||
private bool IsItemValidIngredient(Item item, FabricationRecipe.RequiredItem requiredItem)
|
||||
{
|
||||
return
|
||||
item != null &&
|
||||
requiredItem.ItemPrefabs.Contains(item.prefab) &&
|
||||
item.Condition / item.Prefab.Health >= requiredItem.MinCondition &&
|
||||
item.Condition / item.Prefab.Health <= requiredItem.MaxCondition;
|
||||
availablePrefabs.Remove(availablePrefab);
|
||||
|
||||
if (availablePrefab.ParentInventory == inputContainer.Inventory)
|
||||
{
|
||||
//already in input container, all good
|
||||
usedItems.Add(availablePrefab);
|
||||
}
|
||||
else //in another inventory, we need to move the item
|
||||
{
|
||||
if (!inputContainer.Inventory.CanBePut(availablePrefab))
|
||||
{
|
||||
var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it));
|
||||
unneededItem?.Drop(null, createNetworkEvent: !isClient);
|
||||
}
|
||||
inputContainer.Inventory.TryPutItem(availablePrefab, user: null, createNetworkEvent: !isClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
|
||||
@@ -7,10 +7,15 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class MiniMap : Powered
|
||||
{
|
||||
class HullData
|
||||
internal class HullData
|
||||
{
|
||||
public float? Oxygen;
|
||||
public float? Water;
|
||||
public float? HullOxygenAmount,
|
||||
HullWaterAmount;
|
||||
|
||||
public float? ReceivedOxygenAmount,
|
||||
ReceivedWaterAmount;
|
||||
|
||||
public readonly HashSet<IdCard> Cards = new HashSet<IdCard>();
|
||||
|
||||
public bool Distort;
|
||||
public float DistortionTimer;
|
||||
@@ -45,17 +50,38 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, true, description: "Enable hull status mode.")]
|
||||
public bool EnableHullStatus
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, true, description: "Enable electrical view mode.")]
|
||||
public bool EnableElectricalView
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, true, description: "Enable item finder mode.")]
|
||||
public bool EnableItemFinder
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public MiniMap(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
hullDatas = new Dictionary<Hull, HullData>();
|
||||
InitProjSpecific(element);
|
||||
InitProjSpecific();
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
partial void InitProjSpecific();
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
//periodically reset all hull data
|
||||
//(so that outdated hull info won't be shown if detectors stop sending signals)
|
||||
@@ -65,13 +91,29 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (!hullData.Distort)
|
||||
{
|
||||
hullData.Oxygen = null;
|
||||
hullData.Water = null;
|
||||
hullData.ReceivedOxygenAmount = null;
|
||||
hullData.ReceivedWaterAmount = null;
|
||||
}
|
||||
}
|
||||
resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (cardRefreshTimer > cardRefreshDelay)
|
||||
{
|
||||
if (item.Submarine is { } sub)
|
||||
{
|
||||
UpdateIDCards(sub);
|
||||
}
|
||||
|
||||
cardRefreshTimer = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
cardRefreshTimer += deltaTime;
|
||||
}
|
||||
#endif
|
||||
|
||||
currPowerConsumption = powerConsumption;
|
||||
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
@@ -81,7 +123,7 @@ namespace Barotrauma.Items.Components
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
return picker != null;
|
||||
@@ -99,30 +141,49 @@ namespace Barotrauma.Items.Components
|
||||
hullDatas.Add(sourceHull, hullData);
|
||||
}
|
||||
|
||||
if (hullData.Distort) return;
|
||||
if (hullData.Distort) { return; }
|
||||
|
||||
switch (connection.Name)
|
||||
{
|
||||
case "water_data_in":
|
||||
//cheating a bit because water detectors don't actually send the water level
|
||||
float waterAmount;
|
||||
if (source.GetComponent<WaterDetector>() == null)
|
||||
{
|
||||
hullData.Water = Rand.Range(0.0f, 1.0f);
|
||||
waterAmount = Rand.Range(0.0f, 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
hullData.Water = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f);
|
||||
waterAmount = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f);
|
||||
}
|
||||
hullData.ReceivedWaterAmount = waterAmount;
|
||||
foreach (var linked in sourceHull.linkedTo)
|
||||
{
|
||||
if (!(linked is Hull linkedHull)) { continue; }
|
||||
if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData))
|
||||
{
|
||||
linkedHullData = new HullData();
|
||||
hullDatas.Add(linkedHull, linkedHullData);
|
||||
}
|
||||
linkedHullData.ReceivedWaterAmount = waterAmount;
|
||||
}
|
||||
break;
|
||||
case "oxygen_data_in":
|
||||
float oxy;
|
||||
|
||||
if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out oxy))
|
||||
if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float oxy))
|
||||
{
|
||||
oxy = Rand.Range(0.0f, 100.0f);
|
||||
}
|
||||
|
||||
hullData.Oxygen = oxy;
|
||||
hullData.ReceivedOxygenAmount = oxy;
|
||||
foreach (var linked in sourceHull.linkedTo)
|
||||
{
|
||||
if (!(linked is Hull linkedHull)) { continue; }
|
||||
if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData))
|
||||
{
|
||||
linkedHullData = new HullData();
|
||||
hullDatas.Add(linkedHull, linkedHullData);
|
||||
}
|
||||
linkedHullData.ReceivedOxygenAmount = oxy;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+31
-14
@@ -11,9 +11,12 @@ namespace Barotrauma.Items.Components
|
||||
private float generatedAmount;
|
||||
|
||||
//key = vent, float = total volume of the hull the vent is in and the hulls connected to it
|
||||
private Dictionary<Vent, float> ventList;
|
||||
private List<(Vent vent, float hullVolume)> ventList;
|
||||
|
||||
private float totalHullVolume;
|
||||
|
||||
private float ventUpdateTimer;
|
||||
const float VentUpdateInterval = 5.0f;
|
||||
|
||||
public float CurrFlow
|
||||
{
|
||||
@@ -31,6 +34,8 @@ namespace Barotrauma.Items.Components
|
||||
public OxygenGenerator(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
//randomize update timer so all oxygen generators don't update at the same time
|
||||
ventUpdateTimer = Rand.Range(0.0f, VentUpdateInterval);
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
@@ -64,7 +69,7 @@ namespace Barotrauma.Items.Components
|
||||
//20% condition = 4%
|
||||
CurrFlow *= conditionMult * conditionMult;
|
||||
|
||||
UpdateVents(CurrFlow);
|
||||
UpdateVents(CurrFlow, deltaTime);
|
||||
}
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
@@ -75,7 +80,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private void GetVents()
|
||||
{
|
||||
ventList = new Dictionary<Vent, float>();
|
||||
totalHullVolume = 0.0f;
|
||||
ventList ??= new List<(Vent vent, float hullVolume)>();
|
||||
ventList.Clear();
|
||||
foreach (MapEntity entity in item.linkedTo)
|
||||
{
|
||||
if (!(entity is Item linkedItem)) { continue; }
|
||||
@@ -83,30 +90,40 @@ namespace Barotrauma.Items.Components
|
||||
Vent vent = linkedItem.GetComponent<Vent>();
|
||||
if (vent?.Item.CurrentHull == null) { continue; }
|
||||
|
||||
ventList.Add(vent, 0.0f);
|
||||
foreach (Hull connectedHull in vent.Item.CurrentHull.GetConnectedHulls(includingThis: true, searchDepth: 10, ignoreClosedGaps: true))
|
||||
{
|
||||
totalHullVolume += vent.Item.CurrentHull.Volume;
|
||||
ventList.Add((vent, vent.Item.CurrentHull.Volume));
|
||||
}
|
||||
|
||||
for (int i = 0; i < ventList.Count; i++)
|
||||
{
|
||||
Vent vent = ventList[i].vent;
|
||||
foreach (Hull connectedHull in vent.Item.CurrentHull.GetConnectedHulls(includingThis: false, searchDepth: 3, ignoreClosedGaps: true))
|
||||
{
|
||||
//another vent in the connected hull -> don't add it to this vent's total hull volume
|
||||
if (ventList.Any(v => v.vent != vent && v.vent.Item.CurrentHull == connectedHull)) { continue; }
|
||||
totalHullVolume += connectedHull.Volume;
|
||||
ventList[vent] += connectedHull.Volume;
|
||||
ventList[i] = (ventList[i].vent, ventList[i].hullVolume + connectedHull.Volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVents(float deltaOxygen)
|
||||
|
||||
private void UpdateVents(float deltaOxygen, float deltaTime)
|
||||
{
|
||||
if (ventList == null)
|
||||
if (ventList == null || ventUpdateTimer < 0.0f)
|
||||
{
|
||||
GetVents();
|
||||
ventUpdateTimer = VentUpdateInterval;
|
||||
}
|
||||
ventUpdateTimer -= deltaTime;
|
||||
|
||||
if (!ventList.Any() || totalHullVolume <= 0.0f) { return; }
|
||||
|
||||
foreach (KeyValuePair<Vent, float> v in ventList)
|
||||
foreach ((Vent vent, float hullVolume) in ventList)
|
||||
{
|
||||
if (v.Key?.Item.CurrentHull == null) { continue; }
|
||||
if (vent.Item.CurrentHull == null) { continue; }
|
||||
|
||||
v.Key.OxygenFlow = deltaOxygen * (v.Value / totalHullVolume);
|
||||
v.Key.IsActive = true;
|
||||
vent.OxygenFlow = deltaOxygen * (hullVolume / totalHullVolume);
|
||||
vent.IsActive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ namespace Barotrauma.Items.Components
|
||||
public bool HasPower => IsActive && Voltage >= MinVoltage;
|
||||
public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f;
|
||||
|
||||
private const float TinkeringSpeedIncrease = 1.5f;
|
||||
|
||||
public Pump(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -105,11 +107,19 @@ namespace Barotrauma.Items.Components
|
||||
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, 1.0f);
|
||||
|
||||
currFlow = flowPercentage / 100.0f * maxFlow * powerFactor;
|
||||
|
||||
if (item.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering)
|
||||
{
|
||||
currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease;
|
||||
}
|
||||
|
||||
//less effective when in a bad condition
|
||||
currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
item.CurrentHull.WaterVolume += currFlow;
|
||||
if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; }
|
||||
|
||||
Voltage -= deltaTime;
|
||||
}
|
||||
|
||||
public void InfectBallast(string identifier, bool allowMultiplePerShip = false)
|
||||
|
||||
@@ -367,23 +367,19 @@ namespace Barotrauma.Items.Components
|
||||
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.CurrentHull != null)
|
||||
{
|
||||
var aiTarget = item.CurrentHull.AiTarget;
|
||||
if (aiTarget != null && MaxPowerOutput > 0)
|
||||
{
|
||||
float range = Math.Abs(currPowerConsumption) / MaxPowerOutput;
|
||||
float noise = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range);
|
||||
aiTarget.SoundRange = Math.Max(aiTarget.SoundRange, noise);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.AiTarget != null && MaxPowerOutput > 0)
|
||||
{
|
||||
var aiTarget = item.AiTarget;
|
||||
float range = Math.Abs(currPowerConsumption) / MaxPowerOutput;
|
||||
aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range);
|
||||
if (item.CurrentHull != null)
|
||||
{
|
||||
var hullAITarget = item.CurrentHull.AiTarget;
|
||||
if (hullAITarget != null)
|
||||
{
|
||||
hullAITarget.SoundRange = Math.Max(hullAITarget.SoundRange, aiTarget.SoundRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,15 +494,12 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
float prevFireTimer = fireTimer;
|
||||
fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
|
||||
|
||||
|
||||
#if SERVER
|
||||
if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedConstruction == item)
|
||||
{
|
||||
GameMain.Server.KarmaManager.OnReactorOverHeating(blameOnBroken.Character, deltaTime);
|
||||
GameMain.Server.KarmaManager.OnReactorOverHeating(item, blameOnBroken.Character, deltaTime);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (fireTimer >= FireDelay && prevFireTimer < fireDelay)
|
||||
{
|
||||
new FireSource(item.WorldPosition);
|
||||
@@ -595,7 +588,7 @@ namespace Barotrauma.Items.Components
|
||||
GameServer.Log("Reactor meltdown!", ServerLog.MessageType.ItemInteraction);
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
GameMain.Server.KarmaManager.OnReactorMeltdown(blameOnBroken?.Character);
|
||||
GameMain.Server.KarmaManager.OnReactorMeltdown(item, blameOnBroken?.Character);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
@@ -364,6 +364,16 @@ namespace Barotrauma.Items.Components
|
||||
float velY = MathHelper.Lerp((neutralBallastLevel * 100 - 50) * 2, -100 * Math.Sign(targetVelocity.Y), Math.Abs(targetVelocity.Y) / 100.0f);
|
||||
item.SendSignal(new Signal(velY.ToString(CultureInfo.InvariantCulture), sender: user), "velocity_y_out");
|
||||
|
||||
// converts the controlled sub's velocity to km/h and sends it.
|
||||
if (controlledSub is { } sub)
|
||||
{
|
||||
item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.X * Physics.DisplayToRealWorldRatio) * 3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_x");
|
||||
item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.Y * Physics.DisplayToRealWorldRatio) * -3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_y");
|
||||
|
||||
item.SendSignal(new Signal((sub.WorldPosition.X * Physics.DisplayToRealWorldRatio).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_x");
|
||||
item.SendSignal(new Signal(sub.RealWorldDepth.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_y");
|
||||
}
|
||||
|
||||
// if our tactical AI pilot has left, revert back to maintaining position
|
||||
if (navigateTactically && (user == null || user.SelectedConstruction != item))
|
||||
{
|
||||
@@ -382,8 +392,7 @@ namespace Barotrauma.Items.Components
|
||||
float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f;
|
||||
user.Info.IncreaseSkillLevel(
|
||||
"helm",
|
||||
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime,
|
||||
user.Position + Vector2.UnitY * 150.0f);
|
||||
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime);
|
||||
}
|
||||
|
||||
private void UpdateAutoPilot(float deltaTime)
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace Barotrauma.Items.Components
|
||||
//a list of connections a given connection is connected to, either directly or via other power transfer components
|
||||
private readonly Dictionary<Connection, HashSet<Connection>> connectedRecipients = new Dictionary<Connection, HashSet<Connection>>();
|
||||
|
||||
private float overloadCooldownTimer;
|
||||
private const float OverloadCooldown = 5.0f;
|
||||
|
||||
protected float powerLoad;
|
||||
|
||||
protected bool isBroken;
|
||||
@@ -173,12 +176,19 @@ namespace Barotrauma.Items.Components
|
||||
Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage;
|
||||
if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
||||
{
|
||||
if (overloadCooldownTimer > 0.0f)
|
||||
{
|
||||
overloadCooldownTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
|
||||
//damage the item if voltage is too high (except if running as a client)
|
||||
float prevCondition = item.Condition;
|
||||
item.Condition -= deltaTime * 10.0f;
|
||||
|
||||
if (item.Condition <= 0.0f && prevCondition > 0.0f)
|
||||
{
|
||||
overloadCooldownTimer = OverloadCooldown;
|
||||
#if CLIENT
|
||||
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull);
|
||||
Vector2 baseVel = Rand.Vector(300.0f);
|
||||
@@ -370,5 +380,12 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
connectedRecipients?.Clear();
|
||||
connectionDirty?.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Can the item stick even to deflective targets.")]
|
||||
public bool StickToDeflective
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+
|
||||
"It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")]
|
||||
public bool Hitscan
|
||||
@@ -201,7 +208,7 @@ namespace Barotrauma.Items.Components
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
Attack = new Attack(subElement, item.Name + ", Projectile");
|
||||
Attack = new Attack(subElement, item.Name + ", Projectile", item);
|
||||
}
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
@@ -227,10 +234,14 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private void Launch(Character user, Vector2 simPosition, float rotation)
|
||||
private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f)
|
||||
{
|
||||
Item.body.ResetDynamics();
|
||||
Item.SetTransform(simPosition, rotation);
|
||||
if (Attack != null)
|
||||
{
|
||||
Attack.DamageMultiplier = damageMultiplier;
|
||||
}
|
||||
// Set user for hitscan projectiles to work properly.
|
||||
User = user;
|
||||
// Need to set null for non-characterusable items.
|
||||
@@ -243,7 +254,7 @@ namespace Barotrauma.Items.Components
|
||||
Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians));
|
||||
}
|
||||
|
||||
public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List<Body> ignoredBodies, bool createNetworkEvent)
|
||||
public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List<Body> ignoredBodies, bool createNetworkEvent, float damageMultiplier = 1f)
|
||||
{
|
||||
//add the limbs of the shooter to the list of bodies to be ignored
|
||||
//so that the player can't shoot himself
|
||||
@@ -264,7 +275,7 @@ namespace Barotrauma.Items.Components
|
||||
projectilePos = newPos;
|
||||
}
|
||||
}
|
||||
Launch(user, projectilePos, rotation);
|
||||
Launch(user, projectilePos, rotation, damageMultiplier);
|
||||
if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
#if SERVER
|
||||
@@ -329,7 +340,7 @@ namespace Barotrauma.Items.Components
|
||||
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
|
||||
}
|
||||
|
||||
item.Drop(null);
|
||||
item.Drop(null, createNetworkEvent: false);
|
||||
|
||||
launchPos = item.SimPosition;
|
||||
|
||||
@@ -355,6 +366,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
float rotation = item.body.Rotation;
|
||||
Vector2 simPositon = item.SimPosition;
|
||||
Vector2 rayStartWorld = item.WorldPosition;
|
||||
item.Drop(null);
|
||||
|
||||
item.body.Enabled = true;
|
||||
@@ -366,7 +378,6 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 rayStart = simPositon;
|
||||
Vector2 rayEnd = rayStart + dir * 500.0f;
|
||||
|
||||
Vector2 rayStartWorld = item.WorldPosition;
|
||||
float worldDist = 1000.0f;
|
||||
#if CLIENT
|
||||
worldDist = Screen.Selected?.Cam?.WorldView.Width ?? GameMain.GraphicsWidth;
|
||||
@@ -578,7 +589,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (!removePending)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
Entity useTarget = lastTarget?.Body.UserData is Limb limb ? limb.character : lastTarget?.Body.UserData as Entity;
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, useTarget: useTarget, user: _user);
|
||||
}
|
||||
|
||||
if (item.body != null && item.body.FarseerBody.IsBullet)
|
||||
@@ -602,10 +614,17 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
//target very far from the item -> update the item's transform to make sure it's inside the same sub as the target (or outside)
|
||||
if (Math.Abs(stickJoint.JointTranslation) > 100.0f)
|
||||
{
|
||||
item.UpdateTransform();
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
if (StickTargetRemoved() ||
|
||||
(!StickPermanently && (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f)))
|
||||
(!StickPermanently && (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f)) ||
|
||||
Math.Abs(stickJoint.JointTranslation) > 100.0f) //failsafe unstick if the target is still extremely far
|
||||
{
|
||||
Unstick();
|
||||
#if SERVER
|
||||
@@ -623,7 +642,6 @@ namespace Barotrauma.Items.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private bool OnProjectileCollision(Fixture f1, Fixture target, Contact contact)
|
||||
{
|
||||
if (User != null && User.Removed) { User = null; return false; }
|
||||
@@ -694,7 +712,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
||||
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
||||
private Fixture lastTarget;
|
||||
|
||||
private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity)
|
||||
{
|
||||
@@ -705,6 +724,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
return false;
|
||||
}
|
||||
lastTarget = target;
|
||||
|
||||
float projectileNewSpeed = 0.5f;
|
||||
float projectileDeflectedNewSpeed = 0.1f;
|
||||
@@ -835,14 +855,16 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
if (attackResult.AppliedDamageModifiers != null &&
|
||||
attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles))
|
||||
(attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective))
|
||||
{
|
||||
item.body.LinearVelocity *= projectileDeflectedNewSpeed;
|
||||
}
|
||||
else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit &&
|
||||
else if ( // When hitting characters the collision normal seems to sometimes point into wrong direction, resulting in a failed attempt to stick
|
||||
//Vector2.Dot(Vector2.Normalize(velocity), collisionNormal) < 0.0f &&
|
||||
hits.Count() >= MaxTargetsToHit &&
|
||||
target.Body.Mass > item.body.Mass * 0.5f &&
|
||||
(DoesStick ||
|
||||
(StickToCharacters && target.Body.UserData is Limb) ||
|
||||
(StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
|
||||
(StickToStructures && target.Body.UserData is Structure) ||
|
||||
(StickToItems && target.Body.UserData is Item)))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Quality : ItemComponent
|
||||
{
|
||||
public const int MaxQuality = 3;
|
||||
|
||||
public static readonly float[] QualityCommonnesses = new float[]
|
||||
{
|
||||
0.8f,
|
||||
0.15f,
|
||||
0.045f,
|
||||
0.005f,
|
||||
};
|
||||
|
||||
public enum StatType
|
||||
{
|
||||
Condition,
|
||||
ExplosionRadius,
|
||||
ExplosionDamage,
|
||||
RepairSpeed,
|
||||
RepairToolStructureRepairMultiplier,
|
||||
RepairToolStructureDamageMultiplier,
|
||||
RepairToolDeattachTimeMultiplier,
|
||||
// unused as of now
|
||||
AttackMultiplier,
|
||||
AttackSpeedMultiplier,
|
||||
ForceDoorsOpenSpeedMultiplier,
|
||||
RangedSpreadReduction,
|
||||
ChargeSpeedMultiplier,
|
||||
MovementSpeedMultiplier,
|
||||
// generic stats to be used for various needs, declared just in case (localization)
|
||||
EffectivenessMultiplier,
|
||||
PowerOutputMultiplier,
|
||||
ConsumptionReductionMultiplier,
|
||||
}
|
||||
|
||||
private readonly Dictionary<StatType, float> statValues = new Dictionary<StatType, float>();
|
||||
|
||||
private int qualityLevel;
|
||||
|
||||
[Serialize(0, true)]
|
||||
public int QualityLevel
|
||||
{
|
||||
get { return qualityLevel; }
|
||||
set
|
||||
{
|
||||
if (value == qualityLevel) { return; }
|
||||
|
||||
bool wasInFullCondition = item.IsFullCondition;
|
||||
qualityLevel = MathHelper.Clamp(value, 0, MaxQuality);
|
||||
//set the condition to the new max condition
|
||||
if (wasInFullCondition && statValues.ContainsKey(StatType.Condition))
|
||||
{
|
||||
item.Condition = item.MaxCondition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Quality(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLower())
|
||||
{
|
||||
case "stattype":
|
||||
case "statvalue":
|
||||
case "qualitystat":
|
||||
string statTypeString = subElement.GetAttributeString("stattype", "");
|
||||
if (!Enum.TryParse(statTypeString, true, out StatType statType))
|
||||
{
|
||||
DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + item.prefab.Identifier + ")");
|
||||
}
|
||||
float statValue = subElement.GetAttributeFloat("value", 0f);
|
||||
statValues.TryAdd(statType, statValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float GetValue(StatType statType)
|
||||
{
|
||||
if (!statValues.ContainsKey(statType)) { return 0.0f; }
|
||||
return statValues[statType] * qualityLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class RemoteController : ItemComponent
|
||||
{
|
||||
[Serialize("", false, description: "Tag or identifier of the item that should be controlled.")]
|
||||
public string Target
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool OnlyInOwnSub
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(10000.0f, false)]
|
||||
public float Range
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Item TargetItem { get => currentTarget; }
|
||||
|
||||
private Item currentTarget;
|
||||
private Character currentUser;
|
||||
private Submarine currentSub;
|
||||
|
||||
public RemoteController(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Select(Character character)
|
||||
{
|
||||
if (base.Select(character))
|
||||
{
|
||||
FindTarget(character);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void Equip(Character character)
|
||||
{
|
||||
FindTarget(character);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
base.Update(deltaTime, cam);
|
||||
if (currentTarget.Removed ||
|
||||
item.Submarine != currentSub ||
|
||||
Vector2.DistanceSquared(currentTarget.WorldPosition, item.WorldPosition) > Range * Range)
|
||||
{
|
||||
FindTarget(currentUser);
|
||||
}
|
||||
}
|
||||
|
||||
private void FindTarget(Character user)
|
||||
{
|
||||
currentTarget = null;
|
||||
if (user == null || (item.Submarine == null && OnlyInOwnSub))
|
||||
{
|
||||
IsActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
float closestDist = float.PositiveInfinity;
|
||||
foreach (Item targetItem in Item.ItemList)
|
||||
{
|
||||
if (OnlyInOwnSub)
|
||||
{
|
||||
if (targetItem.Submarine != item.Submarine) { continue; }
|
||||
if (targetItem.Submarine.TeamID != user.TeamID) { continue; }
|
||||
}
|
||||
if (!targetItem.HasTag(Target) && targetItem.prefab.Identifier != Target) { continue; }
|
||||
|
||||
float distSqr = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition);
|
||||
if (distSqr > Range * Range || distSqr > closestDist) { continue; }
|
||||
|
||||
currentTarget = targetItem;
|
||||
currentSub = item.Submarine;
|
||||
closestDist = distSqr;
|
||||
currentUser = user;
|
||||
}
|
||||
IsActive = currentTarget != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Repairable : ItemComponent, IServerSerializable, IClientSerializable
|
||||
{
|
||||
private string header;
|
||||
private readonly string header;
|
||||
|
||||
private float deteriorationTimer;
|
||||
private float deteriorateAlwaysResetTimer;
|
||||
@@ -85,10 +85,10 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
private float skillRequirementMultiplier;
|
||||
|
||||
|
||||
[Serialize(1.0f, true)]
|
||||
public float SkillRequirementMultiplier
|
||||
{
|
||||
public float SkillRequirementMultiplier
|
||||
{
|
||||
get { return skillRequirementMultiplier; }
|
||||
set
|
||||
{
|
||||
@@ -100,21 +100,30 @@ namespace Barotrauma.Items.Components
|
||||
RecreateGUI();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsTinkering { get; private set; } = false;
|
||||
|
||||
public float RepairIconThreshold
|
||||
{
|
||||
get { return RepairThreshold / 2; }
|
||||
}
|
||||
|
||||
public Character CurrentFixer { get; private set; }
|
||||
private Item currentRepairItem;
|
||||
|
||||
private float tinkeringDuration;
|
||||
private float tinkeringStrength;
|
||||
|
||||
public float TinkeringStrength => tinkeringStrength;
|
||||
|
||||
public enum FixActions : int
|
||||
{
|
||||
None = 0,
|
||||
Repair = 1,
|
||||
Sabotage = 2
|
||||
Sabotage = 2,
|
||||
Tinker = 3,
|
||||
}
|
||||
|
||||
private FixActions currentFixerAction = FixActions.None;
|
||||
@@ -131,13 +140,13 @@ namespace Barotrauma.Items.Components
|
||||
canBeSelected = true;
|
||||
|
||||
this.item = item;
|
||||
header =
|
||||
header =
|
||||
TextManager.Get(element.GetAttributeString("header", ""), returnNull: true) ??
|
||||
TextManager.Get(item.Prefab.ConfigElement.GetAttributeString("header", ""), returnNull: true) ??
|
||||
element.GetAttributeString("name", "");
|
||||
|
||||
//backwards compatibility
|
||||
var repairThresholdAttribute =
|
||||
var repairThresholdAttribute =
|
||||
element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("showrepairuithreshold", StringComparison.OrdinalIgnoreCase)) ??
|
||||
element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("airepairthreshold", StringComparison.OrdinalIgnoreCase));
|
||||
if (repairThresholdAttribute != null)
|
||||
@@ -161,12 +170,14 @@ namespace Barotrauma.Items.Components
|
||||
/// <summary>
|
||||
/// Check if the character manages to succesfully repair the item
|
||||
/// </summary>
|
||||
public bool CheckCharacterSuccess(Character character)
|
||||
public bool CheckCharacterSuccess(Character character, Item bestRepairItem)
|
||||
{
|
||||
if (character == null) { return false; }
|
||||
|
||||
if (statusEffectLists == null || statusEffectLists.None(s => s.Key == ActionType.OnFailure)) { return true; }
|
||||
|
||||
if (bestRepairItem != null && bestRepairItem.Prefab.CannotRepairFail) { return true; }
|
||||
|
||||
// unpowered (electrical) items can be repaired without a risk of electrical shock
|
||||
if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase)) &&
|
||||
item.GetComponent<Powered>() is Powered powered && powered.Voltage < 0.1f) { return true; }
|
||||
@@ -174,6 +185,10 @@ namespace Barotrauma.Items.Components
|
||||
if (Rand.Range(0.0f, 0.5f) < RepairDegreeOfSuccess(character, requiredSkills)) { return true; }
|
||||
|
||||
ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
|
||||
if (bestRepairItem != null && bestRepairItem.GetComponent<Holdable>() is Holdable h)
|
||||
{
|
||||
h.ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -191,7 +206,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
return ((average + 100.0f) / 2.0f) / 100.0f;
|
||||
}
|
||||
|
||||
|
||||
public bool StartRepairing(Character character, FixActions action)
|
||||
{
|
||||
if (character == null || character.IsDead || action == FixActions.None)
|
||||
@@ -201,13 +216,19 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
Item bestRepairItem = GetBestRepairItem(character);
|
||||
#if SERVER
|
||||
if (CurrentFixer != character || currentFixerAction != action)
|
||||
{
|
||||
if (!CheckCharacterSuccess(character))
|
||||
if (!CheckCharacterSuccess(character, bestRepairItem))
|
||||
{
|
||||
GameServer.Log($"{GameServer.CharacterLogName(character)} failed to {(action == FixActions.Sabotage ? "sabotage" : "repair")} {item.Name}", ServerLog.MessageType.ItemInteraction);
|
||||
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID });
|
||||
if (bestRepairItem != null && bestRepairItem.GetComponent<Holdable>() is Holdable h)
|
||||
{
|
||||
GameMain.Server?.CreateEntityEvent(bestRepairItem, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, h, character.ID });
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -215,11 +236,32 @@ namespace Barotrauma.Items.Components
|
||||
item.CreateServerEvent(this);
|
||||
}
|
||||
#else
|
||||
if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character)) { return false; }
|
||||
if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character, bestRepairItem)) { return false; }
|
||||
#endif
|
||||
CurrentFixer = character;
|
||||
currentRepairItem = bestRepairItem;
|
||||
CurrentFixerAction = action;
|
||||
if (action == FixActions.Tinker)
|
||||
{
|
||||
tinkeringStrength = 1f + CurrentFixer.GetStatValue(StatTypes.TinkeringStrength);
|
||||
|
||||
if (character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors) && item.GetComponent<Deconstructor>() != null || item.GetComponent<Fabricator>() != null)
|
||||
{
|
||||
// fabricators and deconstructors can be tinkered indefinitely (more or less)
|
||||
tinkeringDuration = float.MaxValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
tinkeringDuration = CurrentFixer.GetStatValue(StatTypes.TinkeringDuration);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
static Item GetBestRepairItem(Character character)
|
||||
{
|
||||
return character.HeldItems.OrderByDescending(i => i.Prefab.AddedRepairSpeedMultiplier).FirstOrDefault();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,12 +275,24 @@ namespace Barotrauma.Items.Components
|
||||
item.CreateServerEvent(this);
|
||||
}
|
||||
#endif
|
||||
if (currentRepairItem != null)
|
||||
{
|
||||
foreach (var ic in currentRepairItem.GetComponents<ItemComponent>())
|
||||
{
|
||||
ic.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, character);
|
||||
}
|
||||
}
|
||||
if (CurrentFixerAction == FixActions.Tinker)
|
||||
{
|
||||
CurrentFixer.CheckTalents(AbilityEffectType.OnStopTinkering);
|
||||
}
|
||||
CurrentFixer.AnimController.Anim = AnimController.Animation.None;
|
||||
CurrentFixer = null;
|
||||
currentRepairItem = null;
|
||||
currentFixerAction = FixActions.None;
|
||||
#if CLIENT
|
||||
repairSoundChannel?.FadeOutAndDispose();
|
||||
repairSoundChannel = null;
|
||||
repairSoundChannel = null;
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
@@ -266,7 +320,10 @@ namespace Barotrauma.Items.Components
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
UpdateProjSpecific(deltaTime);
|
||||
|
||||
IsTinkering = false;
|
||||
|
||||
item.SendSignal($"{(int) item.ConditionPercentage}", "condition_out");
|
||||
|
||||
if (CurrentFixer == null)
|
||||
{
|
||||
if (deteriorateAlwaysResetTimer > 0.0f)
|
||||
@@ -314,6 +371,25 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentFixerAction == FixActions.Tinker)
|
||||
{
|
||||
tinkeringDuration -= deltaTime;
|
||||
// not great to interject it here, should be less reliant on returning
|
||||
|
||||
float conditionDecrease = deltaTime * (CurrentFixer.GetStatValue(StatTypes.TinkeringDamage) / item.Prefab.Health) * 100f;
|
||||
item.Condition -= conditionDecrease;
|
||||
|
||||
if (!CanTinker(CurrentFixer) || tinkeringDuration <= 0f)
|
||||
{
|
||||
StopRepairing(CurrentFixer);
|
||||
}
|
||||
else
|
||||
{
|
||||
IsTinkering = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float successFactor = requiredSkills.Count == 0 ? 1.0f : RepairDegreeOfSuccess(CurrentFixer, requiredSkills);
|
||||
|
||||
//item must have been below the repair threshold for the player to get an achievement or XP for repairing it
|
||||
@@ -327,6 +403,11 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor);
|
||||
fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f;
|
||||
fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed);
|
||||
|
||||
item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer);
|
||||
|
||||
if (currentFixerAction == FixActions.Repair)
|
||||
{
|
||||
if (fixDuration <= 0.0f)
|
||||
@@ -335,7 +416,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
float conditionIncrease = deltaTime / (fixDuration / item.MaxCondition);
|
||||
// scale with prefab's health instead of real health to ensure repair speed remains static with upgrades
|
||||
float conditionIncrease = deltaTime / (fixDuration / item.Prefab.Health);
|
||||
item.Condition += conditionIncrease;
|
||||
#if SERVER
|
||||
GameMain.Server.KarmaManager.OnItemRepaired(CurrentFixer, this, conditionIncrease);
|
||||
@@ -350,13 +432,13 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier);
|
||||
CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier,
|
||||
SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f),
|
||||
CurrentFixer.Position + Vector2.UnitY * 100.0f);
|
||||
SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f));
|
||||
}
|
||||
SteamAchievementManager.OnItemRepaired(item, CurrentFixer);
|
||||
CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete);
|
||||
}
|
||||
deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay);
|
||||
wasBroken = false;
|
||||
wasBroken = false;
|
||||
StopRepairing(CurrentFixer);
|
||||
}
|
||||
}
|
||||
@@ -368,7 +450,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
float conditionDecrease = deltaTime / (fixDuration / item.MaxCondition);
|
||||
// scale with prefab's health instead of real health to ensure sabotage speed remains static with (any) upgrades
|
||||
float conditionDecrease = deltaTime / (fixDuration / item.Prefab.Health);
|
||||
item.Condition -= conditionDecrease;
|
||||
}
|
||||
|
||||
@@ -380,8 +463,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier);
|
||||
CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier,
|
||||
SkillSettings.Current.SkillIncreasePerSabotage / Math.Max(characterSkillLevel, 1.0f),
|
||||
CurrentFixer.Position + Vector2.UnitY * 100.0f);
|
||||
SkillSettings.Current.SkillIncreasePerSabotage / Math.Max(characterSkillLevel, 1.0f));
|
||||
}
|
||||
|
||||
deteriorationTimer = 0.0f;
|
||||
@@ -399,6 +481,45 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private float GetMaxRepairConditionMultiplier(Character character)
|
||||
{
|
||||
if (character == null) { return 1.0f; }
|
||||
// kind of rough to keep this in update, but seems most robust
|
||||
if (requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierMechanical);
|
||||
}
|
||||
if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierElectrical);
|
||||
}
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
private bool IsTinkerable(Character character)
|
||||
{
|
||||
if (!character.HasAbilityFlag(AbilityFlags.CanTinker)) { return false; }
|
||||
if (item.GetComponent<Engine>() != null) { return true; }
|
||||
if (item.GetComponent<Pump>() != null) { return true; }
|
||||
if (item.HasTag("turretammosource")) { return true; }
|
||||
if (!character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors)) { return false; }
|
||||
if (item.GetComponent<Fabricator>() != null) { return true; }
|
||||
if (item.GetComponent<Deconstructor>() != null) { return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
private Affliction GetTinkerExhaustion(Character character)
|
||||
{
|
||||
return character.CharacterHealth.GetAffliction("tinkerexhaustion");
|
||||
}
|
||||
|
||||
private bool CanTinker(Character character)
|
||||
{
|
||||
if (!IsTinkerable(character)) { return false; }
|
||||
if (GetTinkerExhaustion(character) is Affliction tinkerExhaustion && tinkerExhaustion.Strength <= tinkerExhaustion.Prefab.MaxStrength) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
public void AdjustPowerConsumption(ref float powerConsumption)
|
||||
@@ -423,7 +544,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (ic is PowerTransfer pt)
|
||||
{
|
||||
//power transfer items (junction boxes, relays) don't deteriorate if they're no carrying any power
|
||||
//power transfer items (junction boxes, relays) don't deteriorate if they're no carrying any power
|
||||
if (Math.Abs(pt.CurrPowerConsumption) > 0.1f) { return true; }
|
||||
}
|
||||
else if (ic is Engine engine)
|
||||
@@ -484,7 +605,7 @@ namespace Barotrauma.Items.Components
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
//do nothing
|
||||
//Repairables should always stay active, so we don't want to use the default behavior
|
||||
//Repairables should always stay active, so we don't want to use the default behavior
|
||||
//where set_active/set_state signals can disable the component
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
@@ -54,6 +53,27 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, false, description: "Should the rope snap when the character drops the aim?")]
|
||||
public bool SnapWhenNotAimed
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(30.0f, false, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")]
|
||||
public float TargetMinMass
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool LerpForces
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private bool snapped;
|
||||
public bool Snapped
|
||||
{
|
||||
@@ -75,6 +95,10 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
snapped = value;
|
||||
if (!snapped)
|
||||
{
|
||||
snapTimer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +109,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public void Snap() => Snapped = true;
|
||||
|
||||
public void Attach(ISpatialEntity source, Item target)
|
||||
{
|
||||
@@ -92,6 +117,7 @@ namespace Barotrauma.Items.Components
|
||||
System.Diagnostics.Debug.Assert(target != null);
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
Snapped = false;
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition);
|
||||
IsActive = true;
|
||||
}
|
||||
@@ -118,13 +144,15 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 diff = target.WorldPosition - source.WorldPosition;
|
||||
if (diff.LengthSquared() > MaxLength * MaxLength)
|
||||
{
|
||||
Snapped = true;
|
||||
Snap();
|
||||
return;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
item.ResetCachedVisibleSize();
|
||||
#endif
|
||||
var projectile = target.GetComponent<Projectile>();
|
||||
if (projectile == null) { return; }
|
||||
|
||||
if (SnapOnCollision)
|
||||
{
|
||||
@@ -135,28 +163,24 @@ namespace Barotrauma.Items.Components
|
||||
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
var projectile = target?.GetComponent<Projectile>();
|
||||
if (projectile != null)
|
||||
foreach (Body body in projectile.Hits)
|
||||
{
|
||||
foreach (Body body in projectile.Hits)
|
||||
Submarine alreadyHitSub = null;
|
||||
if (body.UserData is Structure hitStructure)
|
||||
{
|
||||
Submarine alreadyHitSub = null;
|
||||
if (body.UserData is Structure hitStructure)
|
||||
{
|
||||
alreadyHitSub = hitStructure.Submarine;
|
||||
}
|
||||
else if (body.UserData is Submarine hitSub)
|
||||
{
|
||||
alreadyHitSub = hitSub;
|
||||
}
|
||||
if (alreadyHitSub != null)
|
||||
{
|
||||
if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; }
|
||||
if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; }
|
||||
}
|
||||
alreadyHitSub = hitStructure.Submarine;
|
||||
}
|
||||
else if (body.UserData is Submarine hitSub)
|
||||
{
|
||||
alreadyHitSub = hitSub;
|
||||
}
|
||||
if (alreadyHitSub != null)
|
||||
{
|
||||
if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; }
|
||||
if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; }
|
||||
}
|
||||
}
|
||||
Submarine targetSub = target?.GetComponent<Projectile>()?.StickTarget?.UserData as Submarine ?? target.Submarine;
|
||||
Submarine targetSub = projectile.StickTarget?.UserData as Submarine ?? target.Submarine;
|
||||
|
||||
if (f.Body?.UserData is MapEntity mapEntity && mapEntity.Submarine != null)
|
||||
{
|
||||
@@ -175,7 +199,7 @@ namespace Barotrauma.Items.Components
|
||||
return true;
|
||||
}) != null)
|
||||
{
|
||||
Snapped = true;
|
||||
Snap();
|
||||
return;
|
||||
}
|
||||
raycastTimer = 0.0f;
|
||||
@@ -183,27 +207,107 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
Vector2 forceDir = diff;
|
||||
if (forceDir.LengthSquared() > 0.01f)
|
||||
float distance = diff.Length();
|
||||
if (distance > 0.001f)
|
||||
{
|
||||
forceDir = Vector2.Normalize(forceDir);
|
||||
}
|
||||
|
||||
if (Math.Abs(ProjectilePullForce) > 0.001f)
|
||||
{
|
||||
var projectile = target.GetComponent<Projectile>();
|
||||
projectile?.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce);
|
||||
projectile.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce);
|
||||
}
|
||||
|
||||
if (Math.Abs(SourcePullForce) > 0.001f)
|
||||
if (projectile.StickTarget != null)
|
||||
{
|
||||
var sourceBody = GetBodyToPull(source);
|
||||
sourceBody?.ApplyForce(forceDir * SourcePullForce);
|
||||
}
|
||||
|
||||
if (Math.Abs(TargetPullForce) > 0.001f)
|
||||
{
|
||||
var targetBody = GetBodyToPull(target);
|
||||
targetBody?.ApplyForce(-forceDir * TargetPullForce);
|
||||
float targetMass = float.MaxValue;
|
||||
Character targetCharacter = null;
|
||||
if (projectile.StickTarget.UserData is Limb targetLimb)
|
||||
{
|
||||
targetCharacter = targetLimb.character;
|
||||
targetMass = targetLimb.ragdoll.Mass;
|
||||
}
|
||||
else if (projectile.StickTarget.UserData is Character character)
|
||||
{
|
||||
targetCharacter = character;
|
||||
targetMass = character.Mass;
|
||||
}
|
||||
else if (projectile.StickTarget.UserData is Item item)
|
||||
{
|
||||
targetMass = projectile.StickTarget.Mass;
|
||||
}
|
||||
if (projectile.StickTarget.BodyType != BodyType.Dynamic)
|
||||
{
|
||||
targetMass = float.MaxValue;
|
||||
}
|
||||
var user = item.GetComponent<Projectile>()?.User;
|
||||
if (targetMass > TargetMinMass)
|
||||
{
|
||||
if (Math.Abs(SourcePullForce) > 0.001f)
|
||||
{
|
||||
var sourceBody = GetBodyToPull(source);
|
||||
if (sourceBody != null)
|
||||
{
|
||||
var targetBody = GetBodyToPull(target);
|
||||
if (targetBody != null && !(targetBody.UserData is Character))
|
||||
{
|
||||
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
|
||||
}
|
||||
float forceMultiplier = 1;
|
||||
if (user != null)
|
||||
{
|
||||
user.AnimController.Hang();
|
||||
if (user.InWater)
|
||||
{
|
||||
if (user.IsRagdolled)
|
||||
{
|
||||
forceMultiplier = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
forceMultiplier = user.IsRagdolled ? 0.1f : 0.4f;
|
||||
// Prevents too easy smashing to the walls
|
||||
forceDir.X /= 4;
|
||||
// Prevents rubberbanding up and down
|
||||
if (forceDir.Y < 0)
|
||||
{
|
||||
forceDir.Y = 0;
|
||||
}
|
||||
}
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
var myCollider = user.AnimController.Collider;
|
||||
var targetCollider = targetCharacter.AnimController.Collider;
|
||||
if (myCollider.LinearVelocity != Vector2.Zero && targetCollider.LinearVelocity != Vector2.Zero)
|
||||
{
|
||||
if (Vector2.Dot(Vector2.Normalize(myCollider.LinearVelocity), Vector2.Normalize(targetCollider.LinearVelocity)) < 0)
|
||||
{
|
||||
myCollider.ApplyForce(targetCollider.LinearVelocity * targetCollider.Mass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) * forceMultiplier : SourcePullForce * forceMultiplier;
|
||||
sourceBody.ApplyForce(forceDir * force);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Math.Abs(TargetPullForce) > 0.001f)
|
||||
{
|
||||
var targetBody = GetBodyToPull(target);
|
||||
if (user != null && targetCharacter != null && !user.AnimController.InWater)
|
||||
{
|
||||
// Prevents rubberbanding horizontally when dragging a corpse.
|
||||
if ((forceDir.X < 0) != (user.AnimController.Dir < 0))
|
||||
{
|
||||
forceDir.X = Math.Clamp(forceDir.X, -0.1f, 0.1f);
|
||||
}
|
||||
}
|
||||
float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance)) : TargetPullForce;
|
||||
targetBody?.ApplyForce(-forceDir * force);
|
||||
targetCharacter?.AnimController.Collider.ApplyForce(-forceDir * force * 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,19 +335,23 @@ namespace Barotrauma.Items.Components
|
||||
return ownerCharacter.AnimController.Collider;
|
||||
}
|
||||
var projectile = targetItem.GetComponent<Projectile>();
|
||||
if (projectile != null)
|
||||
if (projectile != null && projectile.StickTarget != null)
|
||||
{
|
||||
if (projectile.StickTarget?.UserData is Structure structure)
|
||||
if (projectile.StickTarget.UserData is Structure structure)
|
||||
{
|
||||
return structure.Submarine?.PhysicsBody;
|
||||
}
|
||||
else if (projectile.StickTarget?.UserData is Submarine sub)
|
||||
else if (projectile.StickTarget.UserData is Submarine sub)
|
||||
{
|
||||
return sub?.PhysicsBody;
|
||||
return sub.PhysicsBody;
|
||||
}
|
||||
else if (projectile.StickTarget?.UserData is Character character)
|
||||
else if (projectile.StickTarget.UserData is Item item)
|
||||
{
|
||||
return character.AnimController.Collider;
|
||||
return item.body;
|
||||
}
|
||||
else if (projectile.StickTarget.UserData is Limb limb)
|
||||
{
|
||||
return limb.body;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Scanner : ItemComponent
|
||||
{
|
||||
[Serialize(1.0f, false, description: "How long it takes for the scan to be completed.")]
|
||||
public float ScanDuration { get; set; }
|
||||
[Serialize(0.0f, false, description: "How far along the scan is. When the timer goes above ScanDuration, the scan is completed.")]
|
||||
public float ScanTimer
|
||||
{
|
||||
get
|
||||
{
|
||||
return scanTimer;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
if (Holdable == null) { return; }
|
||||
bool wasScanCompletedPreviously = IsScanCompleted;
|
||||
scanTimer = Math.Max(0.0f, value);
|
||||
if (!wasScanCompletedPreviously && IsScanCompleted)
|
||||
{
|
||||
OnScanCompleted?.Invoke(this);
|
||||
}
|
||||
#if SERVER
|
||||
if (wasScanCompletedPreviously != IsScanCompleted || Math.Abs(LastSentScanTimer - scanTimer) > 0.1f)
|
||||
{
|
||||
item.CreateServerEvent(this);
|
||||
LastSentScanTimer = scanTimer;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
[Serialize(1.0f, false, description: "How far the scanner can be from the target for the scan to be successful.")]
|
||||
public float ScanRadius { get; set; }
|
||||
[Serialize(true, false, description: "Should the progress bar always be displayed when the item has been attached.")]
|
||||
public bool AlwaysDisplayProgressBar { get; set; }
|
||||
|
||||
private Holdable Holdable { get; set; }
|
||||
/// <summary>
|
||||
/// Should the progress bar be displayed. Use when AlwaysDisplayProgressBar is set to false.
|
||||
/// </summary>
|
||||
public bool DisplayProgressBar { get; set; } = false;
|
||||
private bool IsScanCompleted => scanTimer >= ScanDuration;
|
||||
|
||||
private float scanTimer;
|
||||
|
||||
public Action<Scanner> OnScanStarted, OnScanCompleted;
|
||||
|
||||
public Scanner(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (Holdable != null && Holdable.Attachable && Holdable.Attached)
|
||||
{
|
||||
if (ScanTimer <= 0.0f)
|
||||
{
|
||||
OnScanStarted?.Invoke(this);
|
||||
}
|
||||
ScanTimer += deltaTime;
|
||||
item.AiTarget?.IncreaseSoundRange(deltaTime, speed: 2.0f);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
ScanTimer = 0.0f;
|
||||
DisplayProgressBar = false;
|
||||
}
|
||||
UpdateProjSpecific();
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific();
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
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");
|
||||
IsActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class ButtonTerminal : ItemComponent
|
||||
{
|
||||
[Editable, Serialize(new string[0], true, description: "Signals sent when the corresponding buttons are pressed.", alwaysUseInstanceValues: true)]
|
||||
public string[] Signals { get; set; }
|
||||
[Editable, Serialize("", true, description: "Identifiers or tags of items that, when contained, allow the terminal buttons to be used. Multiple ones should be separated by commas.", alwaysUseInstanceValues: true)]
|
||||
public string ActivatingItems { get; set; }
|
||||
|
||||
private int RequiredSignalCount { get; set; }
|
||||
private ItemContainer Container { get; set; }
|
||||
private HashSet<ItemPrefab> ActivatingItemPrefabs { get; set; } = new HashSet<ItemPrefab>();
|
||||
|
||||
|
||||
private bool AllowUsingButtons => ActivatingItemPrefabs.None() || Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab));
|
||||
|
||||
public ButtonTerminal(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
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!");
|
||||
}
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
|
||||
if (Signals == null)
|
||||
{
|
||||
Signals = new string[RequiredSignalCount];
|
||||
for (int i = 0; i < RequiredSignalCount; i++)
|
||||
{
|
||||
Signals[i] = string.Empty;
|
||||
}
|
||||
}
|
||||
else if (Signals.Length != RequiredSignalCount)
|
||||
{
|
||||
string[] newSignals = new string[RequiredSignalCount];
|
||||
if (Signals.Length < RequiredSignalCount)
|
||||
{
|
||||
Signals.CopyTo(newSignals, 0);
|
||||
for (int i = Signals.Length; i < RequiredSignalCount; i++)
|
||||
{
|
||||
newSignals[i] = string.Empty;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < RequiredSignalCount; i++)
|
||||
{
|
||||
newSignals[i] = Signals[i];
|
||||
}
|
||||
}
|
||||
Signals = newSignals;
|
||||
}
|
||||
|
||||
ActivatingItemPrefabs.Clear();
|
||||
if (!string.IsNullOrEmpty(ActivatingItems))
|
||||
{
|
||||
foreach (var activatingItem in ActivatingItems.Split(','))
|
||||
{
|
||||
if (MapEntityPrefab.Find(null, identifier: activatingItem, showErrorMessages: false) is ItemPrefab prefab)
|
||||
{
|
||||
ActivatingItemPrefabs.Add(prefab);
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t.Equals(activatingItem, StringComparison.OrdinalIgnoreCase)))
|
||||
.ForEach(p => ActivatingItemPrefabs.Add(p));
|
||||
}
|
||||
}
|
||||
if (ActivatingItemPrefabs.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no activating item prefabs found with identifiers or tags \"{ActivatingItems}\"");
|
||||
}
|
||||
}
|
||||
|
||||
var containers = item.GetComponents<ItemContainer>().ToList();
|
||||
if (containers.Count != 1)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in item \"{item.Name}\": the ButtonTerminal component requires exactly one ItemContainer component!");
|
||||
return;
|
||||
}
|
||||
Container = containers[0];
|
||||
|
||||
OnItemLoadedProjSpecific();
|
||||
}
|
||||
|
||||
partial void OnItemLoadedProjSpecific();
|
||||
|
||||
private bool SendSignal(int signalIndex, bool isServerMessage = false)
|
||||
{
|
||||
if (!isServerMessage && !AllowUsingButtons) { return false; }
|
||||
string signal = Signals[signalIndex];
|
||||
string connectionName = $"signal_out{signalIndex + 1}";
|
||||
item.SendSignal(signal, connectionName);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Write(IWriteMessage msg, object[] extraData)
|
||||
{
|
||||
if (extraData == null || extraData.Length < 3) { return; }
|
||||
msg.WriteRangedInteger((int)extraData[2], 0, Signals.Length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
foreach (XElement connectionElement in subElement.Elements())
|
||||
{
|
||||
string prefabConnectionName = element.GetAttributeString("name", null);
|
||||
string prefabConnectionName = connectionElement.GetAttributeString("name", null);
|
||||
if (prefabConnectionName == Name)
|
||||
{
|
||||
displayNameTag = connectionElement.GetAttributeString("displayname", "");
|
||||
|
||||
+24
-13
@@ -134,20 +134,30 @@ namespace Barotrauma.Items.Components
|
||||
foreach (Wire wire in c.Wires)
|
||||
{
|
||||
if (wire == null) { continue; }
|
||||
#if CLIENT
|
||||
if (wire.Item.IsSelected) { continue; }
|
||||
#endif
|
||||
var wireNodes = wire.GetNodes();
|
||||
if (wireNodes.Count == 0) { continue; }
|
||||
TryMoveWire(wire);
|
||||
}
|
||||
}
|
||||
|
||||
if (Submarine.RectContains(item.Rect, wireNodes[0] + wireNodeOffset))
|
||||
{
|
||||
wire.MoveNode(0, amount);
|
||||
}
|
||||
else if (Submarine.RectContains(item.Rect, wireNodes[wireNodes.Count - 1] + wireNodeOffset))
|
||||
{
|
||||
wire.MoveNode(wireNodes.Count - 1, amount);
|
||||
}
|
||||
foreach (var wire in DisconnectedWires)
|
||||
{
|
||||
TryMoveWire(wire);
|
||||
}
|
||||
|
||||
void TryMoveWire(Wire wire)
|
||||
{
|
||||
#if CLIENT
|
||||
if (wire.Item.IsSelected) { return; }
|
||||
#endif
|
||||
var wireNodes = wire.GetNodes();
|
||||
if (wireNodes.Count == 0) { return; }
|
||||
|
||||
if (Submarine.RectContains(item.Rect, wireNodes[0] + wireNodeOffset))
|
||||
{
|
||||
wire.MoveNode(0, amount);
|
||||
}
|
||||
else if (Submarine.RectContains(item.Rect, wireNodes[wireNodes.Count - 1] + wireNodeOffset))
|
||||
{
|
||||
wire.MoveNode(wireNodes.Count - 1, amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,6 +360,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections.Clear();
|
||||
|
||||
#if CLIENT
|
||||
rewireSoundChannel?.FadeOutAndDispose();
|
||||
|
||||
@@ -159,7 +159,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
lightColor = value;
|
||||
#if CLIENT
|
||||
if (Light != null) Light.Color = IsActive ? lightColor : Color.Transparent;
|
||||
if (Light != null)
|
||||
{
|
||||
Light.Color = IsActive ? lightColor : Color.Transparent;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -233,6 +236,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
UpdateOnActiveEffects(deltaTime);
|
||||
|
||||
if (powerIn == null && powerConsumption > 0.0f) { Voltage -= deltaTime; }
|
||||
|
||||
#if CLIENT
|
||||
Light.ParentSub = item.Submarine;
|
||||
#endif
|
||||
@@ -293,8 +298,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
SetLightSourceState(true, lightBrightness);
|
||||
|
||||
if (powerIn == null && powerConsumption > 0.0f) { Voltage -= deltaTime; }
|
||||
}
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
|
||||
+15
-1
@@ -12,8 +12,10 @@ namespace Barotrauma.Items.Components
|
||||
public enum WaveType
|
||||
{
|
||||
Pulse,
|
||||
Sawtooth,
|
||||
Sine,
|
||||
Square,
|
||||
Triangle,
|
||||
}
|
||||
|
||||
private float frequency;
|
||||
@@ -22,8 +24,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
[InGameEditable, Serialize(WaveType.Pulse, true, description: "What kind of a signal the item outputs." +
|
||||
" Pulse: periodically sends out a signal of 1." +
|
||||
" Sawtooth: sends out a periodic wave that increases linearly from 0 to 1." +
|
||||
" Sine: sends out a sine wave oscillating between -1 and 1." +
|
||||
" Square: sends out a signal that alternates between 0 and 1.", alwaysUseInstanceValues: true)]
|
||||
" Square: sends out a signal that alternates between 0 and 1." +
|
||||
" Triangle: sends out a wave that alternates between increasing linearly from -1 to 1 and decreasing from 1 to -1.",
|
||||
alwaysUseInstanceValues: true)]
|
||||
public WaveType OutputType
|
||||
{
|
||||
get;
|
||||
@@ -63,6 +68,10 @@ namespace Barotrauma.Items.Components
|
||||
phase -= pulseInterval;
|
||||
}
|
||||
break;
|
||||
case WaveType.Sawtooth:
|
||||
phase = (phase + deltaTime * frequency) % 1.0f;
|
||||
item.SendSignal(phase.ToString(CultureInfo.InvariantCulture), "signal_out");
|
||||
break;
|
||||
case WaveType.Square:
|
||||
phase = (phase + deltaTime * frequency) % 1.0f;
|
||||
item.SendSignal(phase < 0.5f ? "0" : "1", "signal_out");
|
||||
@@ -71,6 +80,11 @@ namespace Barotrauma.Items.Components
|
||||
phase = (phase + deltaTime * frequency) % 1.0f;
|
||||
item.SendSignal(Math.Sin(phase * MathHelper.TwoPi).ToString(CultureInfo.InvariantCulture), "signal_out");
|
||||
break;
|
||||
case WaveType.Triangle:
|
||||
phase = (phase + deltaTime * frequency) % 1.0f;
|
||||
float output = 4.0f * MathF.Abs(MathUtils.PositiveModulo(phase - 0.25f, 1.0f) - 0.5f) - 1.0f;
|
||||
item.SendSignal(output.ToString(CultureInfo.InvariantCulture), "signal_out");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -80,19 +80,21 @@ namespace Barotrauma.Items.Components
|
||||
public RegExFindComponent(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
nonContinuousOutputSent = true;
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(expression) || regex == null) return;
|
||||
if (string.IsNullOrWhiteSpace(expression) || regex == null) { return; }
|
||||
if (!ContinuousOutput && nonContinuousOutputSent) { return; }
|
||||
|
||||
if (receivedSignal != previousReceivedSignal && receivedSignal != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Match match = regex.Match(receivedSignal);
|
||||
previousResult = match.Success;
|
||||
previousResult = match.Success;
|
||||
previousGroups = UseCaptureGroup && previousResult ? match.Groups : null;
|
||||
previousReceivedSignal = receivedSignal;
|
||||
|
||||
@@ -133,7 +135,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); }
|
||||
}
|
||||
else if (!nonContinuousOutputSent)
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); }
|
||||
nonContinuousOutputSent = true;
|
||||
|
||||
@@ -41,10 +41,13 @@ namespace Barotrauma.Items.Components
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) { return; }
|
||||
ShowOnDisplay(value);
|
||||
ShowOnDisplay(value, addToHistory: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Editable, Serialize(false, true, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)]
|
||||
public bool UseMonospaceFont { get; set; }
|
||||
|
||||
private string OutputValue { get; set; }
|
||||
|
||||
public Terminal(Item item, XElement element)
|
||||
@@ -56,7 +59,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
partial void ShowOnDisplay(string input, bool addToHistory = true);
|
||||
partial void ShowOnDisplay(string input, bool addToHistory);
|
||||
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
@@ -67,14 +70,14 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
string inputSignal = signal.value.Replace("\\n", "\n");
|
||||
ShowOnDisplay(inputSignal);
|
||||
ShowOnDisplay(inputSignal, addToHistory: true);
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
bool isSubEditor = false;
|
||||
#if CLIENT
|
||||
isSubEditor = Screen.Selected != GameMain.SubEditorScreen || GameMain.GameSession?.GameMode is TestGameMode;
|
||||
isSubEditor = Screen.Selected == GameMain.SubEditorScreen || GameMain.GameSession?.GameMode is TestGameMode;
|
||||
#endif
|
||||
|
||||
base.OnItemLoaded();
|
||||
@@ -107,7 +110,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
string msg = componentElement.GetAttributeString("msg" + i, null);
|
||||
if (msg == null) { break; }
|
||||
ShowOnDisplay(msg);
|
||||
ShowOnDisplay(msg, addToHistory: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +100,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (item.CurrentHull != null)
|
||||
{
|
||||
int waterPercentage = MathHelper.Clamp((int)Math.Round(item.CurrentHull.WaterPercentage), 0, 100);
|
||||
int waterPercentage = MathHelper.Clamp((int)Math.Ceiling(item.CurrentHull.WaterPercentage), 0, 100);
|
||||
item.SendSignal(waterPercentage.ToString(), "water_%");
|
||||
}
|
||||
string highPressureOut = (item.CurrentHull == null || item.CurrentHull.LethalPressure > 5.0f) ? "1" : "0";
|
||||
item.SendSignal(highPressureOut, "high_pressure");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ namespace Barotrauma.Items.Components
|
||||
//signal strength diminishes by distance
|
||||
float sentSignalStrength = signal.strength *
|
||||
MathHelper.Clamp(1.0f - (Vector2.Distance(item.WorldPosition, wifiComp.item.WorldPosition) / wifiComp.range), 0.0f, 1.0f);
|
||||
Signal s = new Signal(signal.value, ++signal.stepsTaken, sender: signal.sender, source: signal.source,
|
||||
Signal s = new Signal(signal.value, signal.stepsTaken + 1, sender: signal.sender, source: signal.source,
|
||||
power: 0.0f, strength: sentSignalStrength);
|
||||
|
||||
if (wifiComp.signalOutConnection != null)
|
||||
|
||||
@@ -133,15 +133,15 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public bool IsConnectedTo(Item item)
|
||||
{
|
||||
if (connections[0] != null && connections[0].Item == item) return true;
|
||||
return (connections[1] != null && connections[1].Item == item);
|
||||
if (connections[0] != null && connections[0].Item == item) { return true; }
|
||||
return connections[1] != null && connections[1].Item == item;
|
||||
}
|
||||
|
||||
public void RemoveConnection(Item item)
|
||||
{
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (connections[i] == null || connections[i].Item != item) continue;
|
||||
if (connections[i] == null || connections[i].Item != item) { continue; }
|
||||
|
||||
foreach (Wire wire in connections[i].Wires)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Contacts;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class TriggerComponent : ItemComponent
|
||||
{
|
||||
[Editable, Serialize(0.0f, true, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
|
||||
public float Force { get; set; }
|
||||
|
||||
public PhysicsBody PhysicsBody { get; private set; }
|
||||
private float Radius { get; set; }
|
||||
private float RadiusInDisplayUnits { get; set; }
|
||||
private bool TriggeredOnce { get; set; }
|
||||
|
||||
public bool TriggerActive { get; private set; }
|
||||
|
||||
private readonly LevelTrigger.TriggererType triggeredBy;
|
||||
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
|
||||
private readonly bool triggerOnce;
|
||||
private readonly List<ISerializableEntity> statusEffectTargets = new List<ISerializableEntity>();
|
||||
/// <summary>
|
||||
/// Effects applied to entities inside the trigger
|
||||
/// </summary>
|
||||
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
|
||||
/// <summary>
|
||||
/// Attacks applied to entities inside the trigger
|
||||
/// </summary>
|
||||
private readonly List<Attack> attacks = new List<Attack>();
|
||||
|
||||
public TriggerComponent(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
triggerOnce = element.GetAttributeBool("triggeronce", false);
|
||||
string parentDebugName = $"TriggerComponent in {item.Name}";
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "statuseffect":
|
||||
LevelTrigger.LoadStatusEffect(statusEffects, subElement, parentDebugName);
|
||||
break;
|
||||
case "attack":
|
||||
case "damage":
|
||||
LevelTrigger.LoadAttack(subElement, parentDebugName, triggerOnce, attacks);
|
||||
break;
|
||||
}
|
||||
}
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
float radiusAttribute = originalElement.GetAttributeFloat("radius", 10.0f);
|
||||
Radius = ConvertUnits.ToSimUnits(radiusAttribute * item.Scale);
|
||||
PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f)
|
||||
{
|
||||
BodyType = BodyType.Static,
|
||||
CollidesWith = LevelTrigger.GetCollisionCategories(triggeredBy),
|
||||
CollisionCategories = Physics.CollisionWall,
|
||||
UserData = item
|
||||
};
|
||||
PhysicsBody.FarseerBody.SetIsSensor(true);
|
||||
PhysicsBody.FarseerBody.OnCollision += OnCollision;
|
||||
PhysicsBody.FarseerBody.OnSeparation += OnSeparation;
|
||||
RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.radius);
|
||||
}
|
||||
|
||||
public override void OnMapLoaded()
|
||||
{
|
||||
base.OnMapLoaded();
|
||||
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
|
||||
PhysicsBody.Submarine = item.Submarine;
|
||||
}
|
||||
|
||||
private bool OnCollision(Fixture sender, Fixture other, Contact contact)
|
||||
{
|
||||
if (!(LevelTrigger.GetEntity(other) is Entity entity)) { return false; }
|
||||
if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (true, item.Submarine))) { return false; }
|
||||
triggerers.Add(entity);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSeparation(Fixture sender, Fixture other, Contact contact)
|
||||
{
|
||||
if (!(LevelTrigger.GetEntity(other) is Entity entity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (entity is Character character && (!character.Enabled || character.Removed) && triggerers.Contains(entity))
|
||||
{
|
||||
triggerers.Remove(entity);
|
||||
return;
|
||||
}
|
||||
if (LevelTrigger.CheckContactsForOtherFixtures(PhysicsBody, other, entity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
triggerers.Remove(entity);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
triggerers.RemoveWhere(t => t.Removed);
|
||||
LevelTrigger.RemoveDistantTriggerers(PhysicsBody, triggerers, item.WorldPosition);
|
||||
|
||||
if (triggerOnce)
|
||||
{
|
||||
if (TriggeredOnce) { return; }
|
||||
if (triggerers.Count > 0)
|
||||
{
|
||||
TriggeredOnce = true;
|
||||
IsActive = false;
|
||||
triggerers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
TriggerActive = triggerers.Any();
|
||||
|
||||
foreach (Entity triggerer in triggerers)
|
||||
{
|
||||
LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets);
|
||||
|
||||
if (triggerer is IDamageable damageable)
|
||||
{
|
||||
LevelTrigger.ApplyAttacks(attacks, damageable, item.WorldPosition, deltaTime);
|
||||
}
|
||||
else if (triggerer is Submarine submarine)
|
||||
{
|
||||
LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime);
|
||||
}
|
||||
|
||||
if (Force < 0.01f)
|
||||
{
|
||||
// Just ignore very minimal forces
|
||||
continue;
|
||||
}
|
||||
else if (triggerer is Character c)
|
||||
{
|
||||
ApplyForce(c.AnimController.Collider);
|
||||
}
|
||||
else if (triggerer is Submarine s)
|
||||
{
|
||||
ApplyForce(s.SubBody.Body);
|
||||
}
|
||||
else if (triggerer is Item i && i.body != null)
|
||||
{
|
||||
ApplyForce(i.body);
|
||||
}
|
||||
}
|
||||
|
||||
item.SendSignal(IsActive ? "1" : "0", "state_out");
|
||||
}
|
||||
|
||||
private void ApplyForce(PhysicsBody body)
|
||||
{
|
||||
Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition);
|
||||
if (diff.LengthSquared() < 0.0001f) { return; }
|
||||
float distanceFactor = LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits);
|
||||
if (distanceFactor <= 0.0f) { return; }
|
||||
Vector2 force = distanceFactor * Force * Vector2.Normalize(diff);
|
||||
if (force.LengthSquared() < 0.01f) { return; }
|
||||
body.ApplyForce(force);
|
||||
}
|
||||
|
||||
public override void Move(Vector2 amount)
|
||||
{
|
||||
base.Move(amount);
|
||||
if (PhysicsBody != null)
|
||||
{
|
||||
PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
|
||||
PhysicsBody.Submarine = item.Submarine;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,10 @@ namespace Barotrauma.Items.Components
|
||||
private Character currentTarget;
|
||||
const float aiFindTargetInterval = 5.0f;
|
||||
|
||||
private const float TinkeringPowerCostReduction = 0.2f;
|
||||
private const float TinkeringDamageIncrease = 0.2f;
|
||||
private const float TinkeringReloadDecrease = 0.2f;
|
||||
|
||||
public float Rotation
|
||||
{
|
||||
get { return rotation; }
|
||||
@@ -381,6 +385,10 @@ namespace Barotrauma.Items.Components
|
||||
else
|
||||
{
|
||||
float chargeDeltaTime = tryingToCharge ? deltaTime : -deltaTime;
|
||||
if (chargeDeltaTime > 0f && user != null)
|
||||
{
|
||||
chargeDeltaTime *= 1f + user.GetStatValue(StatTypes.TurretChargeSpeed);
|
||||
}
|
||||
currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime);
|
||||
}
|
||||
tryingToCharge = false;
|
||||
@@ -430,8 +438,7 @@ namespace Barotrauma.Items.Components
|
||||
if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedOutpost))
|
||||
{
|
||||
user.Info.IncreaseSkillLevel("weapons",
|
||||
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f),
|
||||
user.Position + Vector2.UnitY * 150.0f);
|
||||
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f));
|
||||
}
|
||||
|
||||
float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f);
|
||||
@@ -504,9 +511,19 @@ namespace Barotrauma.Items.Components
|
||||
return TryLaunch(deltaTime, character);
|
||||
}
|
||||
|
||||
public float GetPowerRequiredToShoot()
|
||||
{
|
||||
float powerCost = powerConsumption;
|
||||
if (user != null)
|
||||
{
|
||||
powerCost /= (1 + user.GetStatValue(StatTypes.TurretPowerCostReduction));
|
||||
}
|
||||
return powerCost;
|
||||
}
|
||||
|
||||
public bool HasPowerToShoot()
|
||||
{
|
||||
return GetAvailableBatteryPower() >= powerConsumption;
|
||||
return GetAvailableBatteryPower() >= GetPowerRequiredToShoot();
|
||||
}
|
||||
|
||||
private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false)
|
||||
@@ -544,6 +561,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
Projectile launchedProjectile = null;
|
||||
bool loaderBroken = false;
|
||||
float tinkeringStrength = 0f;
|
||||
|
||||
for (int i = 0; i < ProjectileCount; i++)
|
||||
{
|
||||
var projectiles = GetLoadedProjectiles();
|
||||
@@ -575,6 +594,7 @@ namespace Barotrauma.Items.Components
|
||||
projectiles = GetLoadedProjectiles();
|
||||
if (projectiles.Any()) { break; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if (projectiles.Count == 0 && !LaunchWithoutProjectile)
|
||||
@@ -601,10 +621,25 @@ namespace Barotrauma.Items.Components
|
||||
return false;
|
||||
}
|
||||
failedLaunchAttempts = 0;
|
||||
|
||||
foreach (MapEntity e in item.linkedTo)
|
||||
{
|
||||
if (!(e is Item linkedItem)) { continue; }
|
||||
if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; }
|
||||
if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource"))
|
||||
{
|
||||
tinkeringStrength = repairable.TinkeringStrength;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ignorePower)
|
||||
{
|
||||
var batteries = item.GetConnectedComponents<PowerContainer>();
|
||||
float neededPower = powerConsumption;
|
||||
float neededPower = GetPowerRequiredToShoot();
|
||||
// tinkering is currently not factored into the common method as it is checked only when shooting
|
||||
// but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later
|
||||
neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction);
|
||||
|
||||
while (neededPower > 0.0001f && batteries.Count > 0)
|
||||
{
|
||||
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
|
||||
@@ -622,7 +657,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
launchedProjectile = projectiles.FirstOrDefault();
|
||||
if (launchedProjectile?.Item.Container != null)
|
||||
Item container = launchedProjectile?.Item.Container;
|
||||
if (container != null)
|
||||
{
|
||||
var repairable = launchedProjectile?.Item.Container.GetComponent<Repairable>();
|
||||
if (repairable != null)
|
||||
@@ -637,18 +673,22 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
foreach (Projectile projectile in projectiles)
|
||||
{
|
||||
Launch(projectile.Item, character);
|
||||
Launch(projectile.Item, character, tinkeringStrength: tinkeringStrength);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Launch(null, character);
|
||||
Launch(null, character, tinkeringStrength: tinkeringStrength);
|
||||
}
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
|
||||
// Turrets also have a light component, which handles the sight range.
|
||||
}
|
||||
if (container != null)
|
||||
{
|
||||
ShiftItemsInProjectileContainer(container.GetComponent<ItemContainer>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,9 +712,15 @@ namespace Barotrauma.Items.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Launch(Item projectile, Character user = null, float? launchRotation = null)
|
||||
private void Launch(Item projectile, Character user = null, float? launchRotation = null, float tinkeringStrength = 0f)
|
||||
{
|
||||
reload = reloadTime;
|
||||
reload /= 1f + (tinkeringStrength * TinkeringReloadDecrease);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
reload /= 1 + user.GetStatValue(StatTypes.TurretAttackSpeed);
|
||||
}
|
||||
|
||||
if (projectile != null)
|
||||
{
|
||||
@@ -697,7 +743,9 @@ namespace Barotrauma.Items.Components
|
||||
Projectile projectileComponent = projectile.GetComponent<Projectile>();
|
||||
if (projectileComponent != null)
|
||||
{
|
||||
projectileComponent.Attacker = user;
|
||||
projectileComponent.Attacker = projectileComponent.User = user;
|
||||
projectileComponent.Attack.DamageMultiplier = 1f + (TinkeringDamageIncrease * tinkeringStrength);
|
||||
|
||||
projectileComponent.Use();
|
||||
projectile.GetComponent<Rope>()?.Attach(item, projectile);
|
||||
projectileComponent.User = user;
|
||||
@@ -725,6 +773,26 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void LaunchProjSpecific();
|
||||
|
||||
private void ShiftItemsInProjectileContainer(ItemContainer container)
|
||||
{
|
||||
if (container == null) { return; }
|
||||
bool moved;
|
||||
do
|
||||
{
|
||||
moved = false;
|
||||
for (int i = 1; i < container.Capacity; i++)
|
||||
{
|
||||
if (container.Inventory.GetItemAt(i) is Item item1 && container.Inventory.CanBePutInSlot(item1, i - 1))
|
||||
{
|
||||
if (container.Inventory.TryPutItem(item1, i - 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true))
|
||||
{
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (moved);
|
||||
}
|
||||
|
||||
private float waitTimer;
|
||||
private float disorderTimer;
|
||||
|
||||
@@ -864,57 +932,26 @@ namespace Barotrauma.Items.Components
|
||||
float turretAngle = -rotation;
|
||||
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; }
|
||||
}
|
||||
|
||||
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
|
||||
Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition);
|
||||
// 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);
|
||||
bool shoot;
|
||||
if (target.Submarine != null)
|
||||
{
|
||||
start -= target.Submarine.SimPosition;
|
||||
end -= target.Submarine.SimPosition;
|
||||
}
|
||||
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
|
||||
var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true,
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
|
||||
return !item.StaticFixtures.Contains(f);
|
||||
});
|
||||
if (pickedBody == null) { return; }
|
||||
Character targetCharacter = null;
|
||||
if (pickedBody.UserData is Character c)
|
||||
{
|
||||
targetCharacter = c;
|
||||
}
|
||||
else if (pickedBody.UserData is Limb limb)
|
||||
{
|
||||
targetCharacter = limb.character;
|
||||
}
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
if (targetCharacter.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Don't shoot friendly characters
|
||||
return;
|
||||
}
|
||||
Body transformedTarget = CheckLineOfSight(start, end);
|
||||
shoot = CanShoot(transformedTarget, user: null, ai, targetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, ai, targetSubmarines));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pickedBody.UserData is ISpatialEntity e)
|
||||
{
|
||||
Submarine sub = e.Submarine;
|
||||
if (sub == null) { return; }
|
||||
if (!targetSubmarines) { return; }
|
||||
if (sub == Item.Submarine) { return; }
|
||||
// Don't shoot non-player submarines, i.e. wrecks or outposts.
|
||||
if (!sub.Info.IsPlayer) { return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hit something else, probably a level wall
|
||||
return;
|
||||
}
|
||||
shoot = CanShoot(worldTarget, user: null, ai, targetSubmarines);
|
||||
}
|
||||
if (shoot)
|
||||
{
|
||||
TryLaunch(deltaTime, ignorePower: true);
|
||||
}
|
||||
TryLaunch(deltaTime, ignorePower: true);
|
||||
}
|
||||
|
||||
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
@@ -998,7 +1035,7 @@ namespace Barotrauma.Items.Components
|
||||
container = containerItem.GetComponent<ItemContainer>();
|
||||
if (container != null) { break; }
|
||||
}
|
||||
if (container == null || container.ContainableItems.Count == 0)
|
||||
if (container == null || !container.ContainableItemIdentifiers.Any())
|
||||
{
|
||||
if (character.IsOnPlayerTeam)
|
||||
{
|
||||
@@ -1022,7 +1059,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (!character.IsOnPlayerTeam) { return; }
|
||||
if (character.Submarine != Submarine.MainSub) { return; }
|
||||
string ammoType = container.ContainableItems.First().Identifiers.FirstOrDefault() ?? "ammobox";
|
||||
string ammoType = container.ContainableItemIdentifiers.FirstOrDefault() ?? "ammobox";
|
||||
int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1);
|
||||
if (remainingAmmo == 0)
|
||||
{
|
||||
@@ -1067,7 +1104,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
// Ignore dead, friendly, and those that are inside the same sub
|
||||
if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; }
|
||||
// Don't aim monsters that are inside a submarine.
|
||||
if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; }
|
||||
// Don't aim monsters that are inside any submarine.
|
||||
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
|
||||
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
|
||||
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
|
||||
@@ -1091,26 +1129,34 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (closestEnemy != null)
|
||||
{
|
||||
// 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.
|
||||
targetPos = closestEnemy.WorldPosition;
|
||||
float closestDist = closestDistance;
|
||||
foreach (Limb limb in closestEnemy.AnimController.Limbs)
|
||||
//if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is
|
||||
if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.Hidden) { continue; }
|
||||
if (!CheckTurretAngle(limb.WorldPosition)) { continue; }
|
||||
float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
|
||||
if (dist < closestDist)
|
||||
{
|
||||
closestDist = dist;
|
||||
targetPos = limb.WorldPosition;
|
||||
}
|
||||
targetPos = closestEnemy.CurrentHull.WorldPosition;
|
||||
}
|
||||
if (closestDist > shootDistance * shootDistance)
|
||||
else
|
||||
{
|
||||
// Not close enough to shoot
|
||||
closestEnemy = null;
|
||||
targetPos = null;
|
||||
// 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;
|
||||
foreach (Limb limb in closestEnemy.AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.Hidden) { continue; }
|
||||
if (!CheckTurretAngle(limb.WorldPosition)) { continue; }
|
||||
float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
|
||||
if (dist < closestDist)
|
||||
{
|
||||
closestDist = dist;
|
||||
targetPos = limb.WorldPosition;
|
||||
}
|
||||
}
|
||||
if (closestDist > shootDistance * shootDistance)
|
||||
{
|
||||
// Not close enough to shoot
|
||||
closestEnemy = null;
|
||||
targetPos = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (item.Submarine != null && Level.Loaded != null)
|
||||
@@ -1158,7 +1204,7 @@ namespace Barotrauma.Items.Components
|
||||
continue;
|
||||
}
|
||||
// Allow targeting farther when heading towards the spire (up to 1000 px)
|
||||
dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); ;
|
||||
dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot));
|
||||
if (dist > closestDistance) { continue; }
|
||||
targetPos = closestPoint;
|
||||
closestDistance = dist;
|
||||
@@ -1222,58 +1268,25 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > maxAngleError) { return false; }
|
||||
|
||||
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
|
||||
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
|
||||
if (closestEnemy != null && closestEnemy.Submarine != null)
|
||||
{
|
||||
start -= closestEnemy.Submarine.SimPosition;
|
||||
end -= closestEnemy.Submarine.SimPosition;
|
||||
}
|
||||
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
|
||||
var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true,
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
|
||||
return !item.StaticFixtures.Contains(f);
|
||||
});
|
||||
if (pickedBody == null) { return false; }
|
||||
Character targetCharacter = null;
|
||||
if (pickedBody.UserData is Character c)
|
||||
{
|
||||
targetCharacter = c;
|
||||
}
|
||||
else if (pickedBody.UserData is Limb limb)
|
||||
{
|
||||
targetCharacter = limb.character;
|
||||
}
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
if (HumanAIController.IsFriendly(character, targetCharacter))
|
||||
{
|
||||
// Don't shoot friendly characters
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pickedBody.UserData is ISpatialEntity e)
|
||||
{
|
||||
Submarine sub = e.Submarine;
|
||||
if (sub == null) { return false; }
|
||||
if (sub == Item.Submarine) { return false; }
|
||||
// Don't shoot non-player submarines, i.e. wrecks or outposts.
|
||||
if (!sub.Info.IsPlayer) { return false; }
|
||||
// Don't shoot friendly submarines.
|
||||
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
|
||||
}
|
||||
else if (!(pickedBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible))
|
||||
{
|
||||
// Hit something else, probably a level wall
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (canShoot)
|
||||
{
|
||||
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
|
||||
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
|
||||
// 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);
|
||||
bool shoot;
|
||||
if (closestEnemy != null && closestEnemy.Submarine != null)
|
||||
{
|
||||
start -= closestEnemy.Submarine.SimPosition;
|
||||
end -= closestEnemy.Submarine.SimPosition;
|
||||
Body transformedTarget = CheckLineOfSight(start, end);
|
||||
shoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
|
||||
}
|
||||
else
|
||||
{
|
||||
shoot = CanShoot(worldTarget, character);
|
||||
}
|
||||
if (!shoot) { return false; }
|
||||
if (character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f);
|
||||
@@ -1284,6 +1297,67 @@ namespace Barotrauma.Items.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool CanShoot(Body targetBody, Character user = null, WreckAI ai = null, bool targetSubmarines = true)
|
||||
{
|
||||
if (targetBody == null) { return false; }
|
||||
Character targetCharacter = null;
|
||||
if (targetBody.UserData is Character c)
|
||||
{
|
||||
targetCharacter = c;
|
||||
}
|
||||
else if (targetBody.UserData is Limb limb)
|
||||
{
|
||||
targetCharacter = limb.character;
|
||||
}
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
if (HumanAIController.IsFriendly(user, targetCharacter))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (ai != null)
|
||||
{
|
||||
if (targetCharacter.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (targetBody.UserData is ISpatialEntity e)
|
||||
{
|
||||
Submarine sub = e.Submarine ?? e as Submarine;
|
||||
if (!targetSubmarines && e is Submarine) { return false; }
|
||||
if (sub == null) { return false; }
|
||||
if (sub == Item.Submarine) { return false; }
|
||||
if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; }
|
||||
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
|
||||
}
|
||||
else if (!(targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible))
|
||||
{
|
||||
// Hit something else, probably a level wall
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Body CheckLineOfSight(Vector2 start, Vector2 end)
|
||||
{
|
||||
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
|
||||
Body pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true,
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
|
||||
return !item.StaticFixtures.Contains(f);
|
||||
});
|
||||
return pickedBody;
|
||||
}
|
||||
|
||||
private Vector2 GetRelativeFiringPosition(bool useOffset = true)
|
||||
{
|
||||
Vector2 transformedFiringOffset = Vector2.Zero;
|
||||
|
||||
@@ -5,8 +5,8 @@ using Barotrauma.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Abilities;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -46,14 +46,23 @@ namespace Barotrauma
|
||||
public LimbType Limb { get; private set; }
|
||||
public bool HideLimb { get; private set; }
|
||||
public bool HideOtherWearables { get; private set; }
|
||||
public bool CanBeHiddenByOtherWearables { get; private set; }
|
||||
public List<WearableType> HideWearablesOfType { get; private set; }
|
||||
public bool InheritLimbDepth { get; private set; }
|
||||
public bool InheritTextureScale { get; private set; }
|
||||
/// <summary>
|
||||
/// Does the wearable inherit all the scalings of the wearer? Also the wearable's own scale is used!
|
||||
/// </summary>
|
||||
public bool InheritScale { get; private set; }
|
||||
public bool IgnoreRagdollScale { get; private set; }
|
||||
public bool IgnoreLimbScale { get; private set; }
|
||||
public bool IgnoreTextureScale { get; private set; }
|
||||
public bool InheritOrigin { get; private set; }
|
||||
public bool InheritSourceRect { get; private set; }
|
||||
|
||||
public float Scale { get; private set; }
|
||||
|
||||
public float Rotation { get; private set; }
|
||||
|
||||
public LimbType DepthLimb { get; private set; }
|
||||
private Wearable _wearableComponent;
|
||||
public Wearable WearableComponent
|
||||
@@ -110,10 +119,9 @@ namespace Barotrauma
|
||||
case WearableType.Husk:
|
||||
case WearableType.Herpes:
|
||||
Limb = LimbType.Head;
|
||||
HideLimb = type == WearableType.Husk || type == WearableType.Herpes;
|
||||
HideOtherWearables = false;
|
||||
InheritLimbDepth = true;
|
||||
InheritTextureScale = true;
|
||||
InheritScale = true;
|
||||
InheritOrigin = true;
|
||||
InheritSourceRect = true;
|
||||
break;
|
||||
@@ -169,13 +177,27 @@ namespace Barotrauma
|
||||
Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true);
|
||||
HideLimb = SourceElement.GetAttributeBool("hidelimb", false);
|
||||
HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false);
|
||||
CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true);
|
||||
InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true);
|
||||
InheritTextureScale = SourceElement.GetAttributeBool("inherittexturescale", false);
|
||||
var scale = SourceElement.GetAttribute("inheritscale");
|
||||
if (scale != null)
|
||||
{
|
||||
InheritScale = scale.GetAttributeBool(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
InheritScale = SourceElement.GetAttributeBool("inherittexturescale", false);
|
||||
}
|
||||
IgnoreLimbScale = SourceElement.GetAttributeBool("ignorelimbscale", false);
|
||||
IgnoreTextureScale = SourceElement.GetAttributeBool("ignoretexturescale", false);
|
||||
IgnoreRagdollScale = SourceElement.GetAttributeBool("ignoreragdollscale", false);
|
||||
SourceElement.GetAttributeBool("inherittexturescale", false);
|
||||
InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", false);
|
||||
InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", false);
|
||||
DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true);
|
||||
Sound = SourceElement.GetAttributeString("sound", "");
|
||||
Scale = SourceElement.GetAttributeFloat("scale", 1.0f);
|
||||
Rotation = MathHelper.ToRadians(SourceElement.GetAttributeFloat("rotation", 0.0f));
|
||||
var index = SourceElement.GetAttributePoint("sheetindex", new Point(-1, -1));
|
||||
if (index.X > -1 && index.Y > -1)
|
||||
{
|
||||
@@ -210,7 +232,9 @@ namespace Barotrauma.Items.Components
|
||||
private readonly Limb[] limb;
|
||||
|
||||
private readonly List<DamageModifier> damageModifiers;
|
||||
public readonly Dictionary<string, float> SkillModifiers;
|
||||
public readonly Dictionary<string, float> SkillModifiers = new Dictionary<string, float>();
|
||||
|
||||
public readonly Dictionary<StatTypes, float> WearableStatValues = new Dictionary<StatTypes, float>();
|
||||
|
||||
public IEnumerable<DamageModifier> DamageModifiers
|
||||
{
|
||||
@@ -266,7 +290,6 @@ namespace Barotrauma.Items.Components
|
||||
this.item = item;
|
||||
|
||||
damageModifiers = new List<DamageModifier>();
|
||||
SkillModifiers = new Dictionary<string, float>();
|
||||
|
||||
int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite");
|
||||
Variants = element.GetAttributeInt("variants", 0);
|
||||
@@ -280,7 +303,7 @@ namespace Barotrauma.Items.Components
|
||||
int i = 0;
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLower())
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "sprite":
|
||||
if (subElement.Attribute("texture") == null)
|
||||
@@ -322,6 +345,18 @@ namespace Barotrauma.Items.Components
|
||||
SkillModifiers.TryAdd(skillIdentifier, skillValue);
|
||||
}
|
||||
break;
|
||||
case "statvalue":
|
||||
StatTypes statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), Name);
|
||||
float statValue = subElement.GetAttributeFloat("value", 0f);
|
||||
if (WearableStatValues.ContainsKey(statType))
|
||||
{
|
||||
WearableStatValues[statType] += statValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
WearableStatValues.TryAdd(statType, statValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,6 +369,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
picker = character;
|
||||
|
||||
for (int i = 0; i < wearableSprites.Length; i++ )
|
||||
{
|
||||
var wearableSprite = wearableSprites[i];
|
||||
@@ -379,19 +415,19 @@ namespace Barotrauma.Items.Components
|
||||
return i1.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(i2.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes));
|
||||
});
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
equipLimb.UpdateWearableTypesToHide();
|
||||
#endif
|
||||
}
|
||||
character.OnWearablesChanged();
|
||||
}
|
||||
|
||||
public override void Drop(Character dropper)
|
||||
{
|
||||
Character previousPicker = picker;
|
||||
Unequip(picker);
|
||||
|
||||
base.Drop(dropper);
|
||||
|
||||
previousPicker?.OnWearablesChanged();
|
||||
picker = null;
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user