Unstable 1.1.14.0
This commit is contained in:
@@ -26,6 +26,8 @@ namespace Barotrauma
|
||||
|
||||
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType>() { InvSlotType.Any };
|
||||
|
||||
public static bool IsHandSlotType(InvSlotType s) => s.HasFlag(InvSlotType.LeftHand) || s.HasFlag(InvSlotType.RightHand);
|
||||
|
||||
protected bool[] IsEquipped;
|
||||
|
||||
/// <summary>
|
||||
@@ -92,7 +94,7 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError($"Character \"{character.SpeciesName}\" is configured to spawn with more items than it has inventory capacity for.");
|
||||
}
|
||||
#if DEBUG
|
||||
else if (itemCount > capacity - 2)
|
||||
else if (itemCount > capacity - 2 && !character.IsPet && capacity > 0)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
$"Character \"{character.SpeciesName}\" is configured to spawn with so many items it will have less than 2 free inventory slots. " +
|
||||
@@ -132,6 +134,13 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
}
|
||||
else if (!character.Enabled)
|
||||
{
|
||||
foreach (var heldItem in character.HeldItems)
|
||||
{
|
||||
if (item.body != null) { item.body.Enabled = false; }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -207,6 +216,10 @@ namespace Barotrauma
|
||||
base.RemoveItem(item);
|
||||
#if CLIENT
|
||||
CreateSlots();
|
||||
if (character == Character.Controlled)
|
||||
{
|
||||
character.SelectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
|
||||
}
|
||||
#endif
|
||||
CharacterHUD.RecreateHudTextsIfControlling(character);
|
||||
//if the item was equipped and there are more items in the same stack, equip one of those items
|
||||
@@ -509,14 +522,20 @@ namespace Barotrauma
|
||||
if (character == Character.Controlled)
|
||||
{
|
||||
HintManager.OnObtainedItem(character, item);
|
||||
character.SelectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
|
||||
}
|
||||
#endif
|
||||
CharacterHUD.RecreateHudTextsIfControlling(character);
|
||||
if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo)
|
||||
{
|
||||
item.CampaignInteractionType = CampaignMode.InteractionType.None;
|
||||
item.AssignCampaignInteractionType(CampaignMode.InteractionType.None);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CreateNetworkEvent(Range slotRange)
|
||||
{
|
||||
GameMain.NetworkMember?.CreateEntityEvent(character, new Character.InventoryStateEventData(slotRange));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,9 +975,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
docked = false;
|
||||
|
||||
Item.Submarine.RefreshOutdoorNodes();
|
||||
Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine);
|
||||
obstructedWayPointsDisabled = false;
|
||||
Item.Submarine.RefreshOutdoorNodes();
|
||||
|
||||
DockingTarget.Undock();
|
||||
DockingTarget = null;
|
||||
@@ -1111,8 +1111,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
if (!obstructedWayPointsDisabled && dockingState >= 0.99f)
|
||||
{
|
||||
Item.Submarine.DisableObstructedWayPoints(DockingTarget?.Item.Submarine);
|
||||
Item.Submarine.RefreshOutdoorNodes();
|
||||
Item.Submarine.DisableObstructedWayPoints(DockingTarget?.Item.Submarine);
|
||||
obstructedWayPointsDisabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
private bool isStuck;
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public bool IsStuck
|
||||
{
|
||||
get { return isStuck; }
|
||||
@@ -49,7 +51,10 @@ namespace Barotrauma.Items.Components
|
||||
if (isStuck == value) { return; }
|
||||
isStuck = value;
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
if (item.FullyInitialized)
|
||||
{
|
||||
item.CreateServerEvent(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -105,7 +110,7 @@ namespace Barotrauma.Items.Components
|
||||
public bool CanBeWelded = true;
|
||||
|
||||
private float stuck;
|
||||
[Serialize(0.0f, IsPropertySaveable.No, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")]
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")]
|
||||
public float Stuck
|
||||
{
|
||||
get { return stuck; }
|
||||
@@ -181,7 +186,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")]
|
||||
public bool HasIntegratedButtons { get; private set; }
|
||||
|
||||
|
||||
[ConditionallyEditable(ConditionallyEditable.ConditionType.HasIntegratedButtons),
|
||||
Serialize(true, IsPropertySaveable.No, description: "If the door has integrated buttons, should clicking on it perform the default action of opening the door? Can be used in conjunction with the \"activate_out\" output to pass a signal to a circuit without toggling the door when someone tries to open/close the door.")]
|
||||
public bool ToggleWhenClicked { get; private set; }
|
||||
|
||||
public float OpenState
|
||||
{
|
||||
get { return openState; }
|
||||
@@ -342,8 +351,12 @@ namespace Barotrauma.Items.Components
|
||||
OnFailedToOpen();
|
||||
return;
|
||||
}
|
||||
item.SendSignal("1", "activate_out");
|
||||
lastUser = user;
|
||||
SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked);
|
||||
if (ToggleWhenClicked)
|
||||
{
|
||||
SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Select(Character character)
|
||||
@@ -389,8 +402,6 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool isClosing = false;
|
||||
if ((!IsStuck && !IsJammed) || !isOpen)
|
||||
{
|
||||
|
||||
@@ -18,8 +18,6 @@ namespace Barotrauma.Items.Components
|
||||
const int MaxNodes = 100;
|
||||
const float MaxNodeDistance = 150.0f;
|
||||
|
||||
private bool waitForVoltageRecalculation;
|
||||
|
||||
public struct Node
|
||||
{
|
||||
public Vector2 WorldPosition;
|
||||
@@ -140,10 +138,6 @@ namespace Barotrauma.Items.Components
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
|
||||
if (character != null && !CharacterUsable) { return false; }
|
||||
|
||||
CurrPowerConsumption = powerConsumption;
|
||||
Voltage = 0.0f;
|
||||
|
||||
waitForVoltageRecalculation = true;
|
||||
charging = true;
|
||||
timer = Duration;
|
||||
IsActive = true;
|
||||
@@ -159,12 +153,6 @@ namespace Barotrauma.Items.Components
|
||||
#if CLIENT
|
||||
frameOffset = Rand.Int(electricitySprite.FrameCount);
|
||||
#endif
|
||||
if (waitForVoltageRecalculation)
|
||||
{
|
||||
waitForVoltageRecalculation = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (timer <= 0.0f)
|
||||
{
|
||||
if (reloadTimer > 0.0f)
|
||||
@@ -179,40 +167,49 @@ namespace Barotrauma.Items.Components
|
||||
timer -= deltaTime;
|
||||
if (charging)
|
||||
{
|
||||
if (GetAvailableInstantaneousBatteryPower() >= PowerConsumption)
|
||||
bool hasPower = false;
|
||||
if (item.Connections == null)
|
||||
{
|
||||
List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
|
||||
float neededPower = PowerConsumption;
|
||||
while (neededPower > 0.0001f && batteries.Count > 0)
|
||||
//no connections and can't be wired = must be powered by something like batteries
|
||||
hasPower = Voltage > MinVoltage;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasPower = GetAvailableInstantaneousBatteryPower() >= PowerConsumption;
|
||||
}
|
||||
|
||||
if (hasPower)
|
||||
{
|
||||
var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f);
|
||||
int batteryCount = batteries.Count();
|
||||
if (batteryCount > 0)
|
||||
{
|
||||
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
|
||||
float takePower = neededPower / batteries.Count;
|
||||
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
|
||||
foreach (PowerContainer battery in batteries)
|
||||
float neededPower = PowerConsumption;
|
||||
while (neededPower > 0.0001f)
|
||||
{
|
||||
neededPower -= takePower;
|
||||
battery.Charge -= takePower / 3600.0f;
|
||||
#if SERVER
|
||||
if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); }
|
||||
#endif
|
||||
float takePower = neededPower / batteryCount;
|
||||
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
|
||||
foreach (PowerContainer battery in batteries)
|
||||
{
|
||||
neededPower -= takePower;
|
||||
battery.Charge -= takePower / 3600.0f;
|
||||
#if SERVER
|
||||
if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
Discharge();
|
||||
|
||||
}
|
||||
else if (Voltage > MinVoltage)
|
||||
{
|
||||
Discharge();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discharge coil only draws power when charging
|
||||
/// Discharge coil doesn't consume grid power, directly takes from the batteries on its grid instead.
|
||||
/// </summary>
|
||||
public override float GetCurrentPowerConsumption(Connection connection = null)
|
||||
public override float GetCurrentPowerConsumption(Connection conn = null)
|
||||
{
|
||||
return charging && IsActive ? PowerConsumption : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
@@ -294,10 +291,24 @@ namespace Barotrauma.Items.Components
|
||||
if (!submarinesInRange.Contains(structure.Submarine)) { continue; }
|
||||
if (OutdoorsOnly)
|
||||
{
|
||||
//check if the structure is within a hull
|
||||
//add a small offset away from the sub's center so structures right at the edge of a hull are still valid
|
||||
Vector2 offset = Vector2.Normalize(structure.WorldPosition - structure.Submarine.WorldPosition);
|
||||
if (Hull.FindHull(structure.Position + offset * Submarine.GridSize, useWorldCoordinates: false) != null) { continue; }
|
||||
//check if there's a hull at either side of the wall
|
||||
Vector2 normal = new Vector2(
|
||||
(float)-Math.Sin(structure.IsHorizontal ? -structure.BodyRotation : MathHelper.PiOver2 - structure.BodyRotation),
|
||||
(float)Math.Cos(structure.IsHorizontal ? -structure.BodyRotation : MathHelper.PiOver2 - structure.BodyRotation));
|
||||
Vector2 structurePos = structure.Position;
|
||||
float offsetAmount = Submarine.GridSize.X * 2;
|
||||
if (structure.HasBody)
|
||||
{
|
||||
structurePos = ConvertUnits.ToDisplayUnits(structure.Bodies.First().Position);
|
||||
offsetAmount = Math.Max(
|
||||
offsetAmount,
|
||||
structure.IsHorizontal ? structure.BodyHeight : structure.BodyWidth);
|
||||
}
|
||||
if (Hull.FindHull(structurePos + normal * offsetAmount, useWorldCoordinates: false) != null &&
|
||||
Hull.FindHull(structurePos - normal * offsetAmount, useWorldCoordinates: false) != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,7 +597,7 @@ namespace Barotrauma.Items.Components
|
||||
case "trigger_in":
|
||||
if (signal.value != "0")
|
||||
{
|
||||
item.Use(1.0f, null);
|
||||
item.Use(1.0f);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components
|
||||
var (x, y, z, w) = Parent.GrowthWeights;
|
||||
float[] weights = { x, y, z, w };
|
||||
|
||||
value = pool.RandomElementByWeight(i => weights[i]);
|
||||
value = pool.GetRandomByWeight(i => weights[i], Rand.RandSync.Unsynced);
|
||||
}
|
||||
|
||||
return (TileSide) (1 << value);
|
||||
@@ -416,8 +416,8 @@ namespace Barotrauma.Items.Components
|
||||
set => health = Math.Clamp(value, 0, MaxHealth);
|
||||
}
|
||||
|
||||
public bool Decayed;
|
||||
public bool FullyGrown;
|
||||
public bool Decayed { get; set; }
|
||||
public bool FullyGrown { get; set; }
|
||||
|
||||
private const int maxProductDelay = 10,
|
||||
maxVineGrowthDelay = 10;
|
||||
@@ -562,7 +562,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (spawnProduct && ProducedItems.Any())
|
||||
{
|
||||
SpawnItem(Item, ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos);
|
||||
SpawnItem(Item, ProducedItems.GetRandomByWeight(it => it.Probability, Rand.RandSync.Unsynced), spawnPos);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,9 @@ namespace Barotrauma.Items.Components
|
||||
#endif
|
||||
public bool DisableHeadRotation { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "If true, this item can't be used if the character is also holding a ranged weapon.")]
|
||||
public bool DisableWhenRangedWeaponEquipped { get; set; }
|
||||
|
||||
[ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")]
|
||||
public float SpriteDepthWhenDropped
|
||||
{
|
||||
@@ -343,11 +346,9 @@ namespace Barotrauma.Items.Components
|
||||
DeattachFromWall();
|
||||
}
|
||||
|
||||
if (setTransform)
|
||||
{
|
||||
if (Pusher != null) { Pusher.Enabled = false; }
|
||||
if (item.body != null) { item.body.Enabled = true; }
|
||||
}
|
||||
if (Pusher != null) { Pusher.Enabled = false; }
|
||||
if (item.body != null) { item.body.Enabled = true; }
|
||||
|
||||
IsActive = false;
|
||||
attachTargetCell = null;
|
||||
|
||||
@@ -616,6 +617,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
//set to submarine-relative position
|
||||
item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition - attachTarget.Submarine.Position), 0.0f, false);
|
||||
body.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
|
||||
}
|
||||
item.Submarine = attachTarget.Submarine;
|
||||
}
|
||||
@@ -688,6 +690,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
if (UsageDisabledByRangedWeapon(character)) { return false; }
|
||||
if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); }
|
||||
if (character != null)
|
||||
{
|
||||
@@ -792,19 +795,27 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position;
|
||||
Vector2 attachPos = userPos + mouseDiff;
|
||||
|
||||
//offset the position by half the size of the grid to get the item to adhere to the grid in the same way as in the sub editor
|
||||
//in the sub editor, we align the top-left corner of the item with the grid
|
||||
//but here the origin of the item is placed at the attach position, so we need to offset it
|
||||
Vector2 offset = new Vector2(
|
||||
-(item.Rect.Width / 2) % Submarine.GridSize.X,
|
||||
(item.Rect.Height / 2) % Submarine.GridSize.Y);
|
||||
|
||||
if (user.Submarine != null)
|
||||
{
|
||||
if (Submarine.PickBody(
|
||||
ConvertUnits.ToSimUnits(user.Position),
|
||||
ConvertUnits.ToSimUnits(user.Position + mouseDiff), collisionCategory: Physics.CollisionWall) != null)
|
||||
{
|
||||
attachPos = userPos + mouseDiff * Submarine.LastPickedFraction;
|
||||
attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset;
|
||||
|
||||
//round down if we're placing on the right side and vice versa: ensures we don't round the position inside a wall
|
||||
return
|
||||
new Vector2(
|
||||
mouseDiff.X > 0 ? (float)Math.Floor(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X,
|
||||
mouseDiff.Y > 0 ? (float)Math.Floor(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.Y);
|
||||
(mouseDiff.X > 0 ? MathF.Floor(attachPos.X / Submarine.GridSize.X) : MathF.Ceiling(attachPos.X / Submarine.GridSize.X)) * Submarine.GridSize.X,
|
||||
(mouseDiff.Y > 0 ? MathF.Floor(attachPos.Y / Submarine.GridSize.Y) : MathF.Ceiling(attachPos.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y)
|
||||
- offset;
|
||||
}
|
||||
}
|
||||
else if (Level.Loaded != null)
|
||||
@@ -827,10 +838,9 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
new Vector2(
|
||||
MathUtils.RoundTowardsClosest(attachPos.X, Submarine.GridSize.X),
|
||||
MathUtils.RoundTowardsClosest(attachPos.Y, Submarine.GridSize.Y));
|
||||
return new Vector2(
|
||||
MathUtils.RoundTowardsClosest(attachPos.X + offset.X, Submarine.GridSize.X),
|
||||
MathUtils.RoundTowardsClosest(attachPos.Y + offset.Y, Submarine.GridSize.Y)) - offset;
|
||||
}
|
||||
|
||||
private Voronoi2.VoronoiCell GetAttachTargetCell(float maxDist)
|
||||
@@ -903,7 +913,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
scaledHandlePos[0] = handlePos[0] * item.Scale;
|
||||
scaledHandlePos[1] = handlePos[1] * item.Scale;
|
||||
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim;
|
||||
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim && !UsageDisabledByRangedWeapon(picker);
|
||||
if (aim)
|
||||
{
|
||||
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle);
|
||||
@@ -963,6 +973,15 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
protected bool UsageDisabledByRangedWeapon(Character character)
|
||||
{
|
||||
if (DisableWhenRangedWeaponEquipped && character != null)
|
||||
{
|
||||
if (character.HeldItems.Any(it => it.GetComponent<RangedWeapon>() != null)) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
//do nothing
|
||||
@@ -1005,14 +1024,12 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
if (item.ParentInventory != null)
|
||||
if (body != null)
|
||||
{
|
||||
if (body != null)
|
||||
{
|
||||
item.body = body;
|
||||
body.Enabled = false;
|
||||
}
|
||||
}
|
||||
body.SetTransformIgnoreContacts(item.SimPosition, item.Rotation);
|
||||
item.body = body;
|
||||
body.Enabled = item.ParentInventory == null;
|
||||
}
|
||||
DeattachFromWall();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,20 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public string OwnerName { get; set; }
|
||||
|
||||
|
||||
private string ownerNameLocalized;
|
||||
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public string OwnerNameLocalized
|
||||
{
|
||||
get { return ownerNameLocalized; }
|
||||
set
|
||||
{
|
||||
if (value.IsNullOrWhiteSpace()) { return; }
|
||||
ownerNameLocalized = value;
|
||||
OwnerName = TextManager.Get(value).Fallback(value).Value;
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public Identifier OwnerJobId { get; set; }
|
||||
|
||||
@@ -67,6 +80,9 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public Vector2 OwnerSheetIndex { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public bool SpawnPointTagsGiven { get; set; }
|
||||
|
||||
public IdCard(Item item, ContentXElement element) : base(item, element) { }
|
||||
|
||||
public void Initialize(WayPoint spawnPoint, Character character)
|
||||
|
||||
@@ -135,7 +135,7 @@ namespace Barotrauma.Items.Components
|
||||
private void CreateTriggerBody()
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(trigger == null, "LevelResource trigger already created!");
|
||||
var body = item.body ?? holdable.Body;
|
||||
var body = item.body ?? holdable?.Body;
|
||||
if (body != null && Attached)
|
||||
{
|
||||
trigger = new PhysicsBody(body.Width, body.Height, body.Radius,
|
||||
|
||||
@@ -189,7 +189,7 @@ namespace Barotrauma.Items.Components
|
||||
impactQueue.Clear();
|
||||
return;
|
||||
}
|
||||
if (picker == null && !picker.HeldItems.Contains(item))
|
||||
if (picker == null || !picker.HeldItems.Contains(item))
|
||||
{
|
||||
impactQueue.Clear();
|
||||
IsActive = false;
|
||||
@@ -218,7 +218,8 @@ namespace Barotrauma.Items.Components
|
||||
AnimController ac = picker.AnimController;
|
||||
if (!hitting)
|
||||
{
|
||||
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim;
|
||||
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim &&
|
||||
!UsageDisabledByRangedWeapon(picker);
|
||||
if (aim)
|
||||
{
|
||||
UpdateSwingPos(deltaTime, out Vector2 swingPos);
|
||||
@@ -364,7 +365,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
hitTargets.Add(targetStructure);
|
||||
}
|
||||
else if (f2.Body.UserData is Item targetItem)
|
||||
else if ((f2.Body.UserData as Item ?? f2.UserData as Item) is Item targetItem)
|
||||
{
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
@@ -410,7 +411,7 @@ namespace Barotrauma.Items.Components
|
||||
Limb targetLimb = target.UserData as Limb;
|
||||
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
|
||||
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
|
||||
Item targetItem = target.UserData as Item;
|
||||
Item targetItem = target.UserData as Item ?? targetFixture.UserData as Item;
|
||||
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
|
||||
if (Attack != null)
|
||||
{
|
||||
|
||||
@@ -73,6 +73,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
//return if someone is already trying to pick the item
|
||||
if (pickTimer > 0.0f) { return false; }
|
||||
if (PickingTime >= float.MaxValue) { return false; }
|
||||
if (picker == null || picker.Inventory == null) { return false; }
|
||||
if (!picker.Inventory.AccessibleWhenAlive && !picker.Inventory.AccessibleByOwner) { return false; }
|
||||
|
||||
@@ -112,10 +113,16 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public virtual bool OnPicked(Character picker)
|
||||
{
|
||||
return OnPicked(picker, pickDroppedStack: true);
|
||||
}
|
||||
|
||||
public virtual bool OnPicked(Character picker, bool pickDroppedStack)
|
||||
{
|
||||
//if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa)
|
||||
if (item.GetComponents<Pickable>().Count() > 0)
|
||||
if (item.GetComponents<Pickable>().Any())
|
||||
{
|
||||
bool alreadyEquipped = false;
|
||||
for (int i = 0; i < picker.Inventory.Capacity; i++)
|
||||
@@ -132,11 +139,13 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
if (alreadyEquipped) { return false; }
|
||||
}
|
||||
var droppedStack = pickDroppedStack ? item.DroppedStack.ToList() : null;
|
||||
if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots))
|
||||
{
|
||||
if (!picker.HeldItems.Contains(item) && item.body != null) { item.body.Enabled = false; }
|
||||
this.picker = picker;
|
||||
|
||||
|
||||
for (int i = item.linkedTo.Count - 1; i >= 0; i--)
|
||||
{
|
||||
item.linkedTo[i].RemoveLinked(item);
|
||||
@@ -147,14 +156,22 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
|
||||
#if CLIENT
|
||||
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItem);
|
||||
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); }
|
||||
PlaySound(ActionType.OnPicked, picker);
|
||||
#endif
|
||||
if (pickDroppedStack)
|
||||
{
|
||||
foreach (var droppedItem in droppedStack)
|
||||
{
|
||||
if (droppedItem == item) { continue; }
|
||||
droppedItem.GetComponent<Pickable>().OnPicked(picker, pickDroppedStack: false);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
|
||||
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItemFail); }
|
||||
#endif
|
||||
|
||||
return false;
|
||||
@@ -183,12 +200,15 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
Character.Controlled?.UpdateHUDProgressBar(
|
||||
this,
|
||||
item.WorldPosition,
|
||||
pickTimer / requiredTime,
|
||||
GUIStyle.Red, GUIStyle.Green,
|
||||
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
|
||||
if (requiredTime < float.MaxValue)
|
||||
{
|
||||
Character.Controlled?.UpdateHUDProgressBar(
|
||||
this,
|
||||
item.WorldPosition,
|
||||
pickTimer / requiredTime,
|
||||
GUIStyle.Red, GUIStyle.Green,
|
||||
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
|
||||
}
|
||||
#endif
|
||||
picker.AnimController.UpdateUseItem(!picker.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
|
||||
pickTimer += CoroutineManager.DeltaTime;
|
||||
@@ -197,12 +217,14 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
StopPicking(picker);
|
||||
|
||||
bool isNotRemote = true;
|
||||
if (!item.Removed)
|
||||
{
|
||||
bool isNotRemote = true;
|
||||
#if CLIENT
|
||||
isNotRemote = !picker.IsRemotePlayer;
|
||||
isNotRemote = !picker.IsRemotePlayer;
|
||||
#endif
|
||||
if (isNotRemote) OnPicked(picker);
|
||||
if (isNotRemote) { OnPicked(picker); }
|
||||
}
|
||||
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
|
||||
@@ -608,6 +608,7 @@ namespace Barotrauma.Items.Components
|
||||
float closestDist = float.MaxValue;
|
||||
foreach (Limb limb in targetCharacter.AnimController.Limbs)
|
||||
{
|
||||
if (limb.Removed || limb.IgnoreCollisions || limb.Hidden || limb.IsSevered) { continue; }
|
||||
float dist = Vector2.DistanceSquared(item.SimPosition, limb.SimPosition);
|
||||
if (dist < closestDist)
|
||||
{
|
||||
@@ -716,7 +717,7 @@ namespace Barotrauma.Items.Components
|
||||
private readonly float repairTimeOut = 5;
|
||||
public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
{
|
||||
if (!(objective.OperateTarget is Gap leak))
|
||||
if (objective.OperateTarget is not Gap leak)
|
||||
{
|
||||
Reset();
|
||||
return true;
|
||||
@@ -806,36 +807,50 @@ namespace Barotrauma.Items.Components
|
||||
// Press the trigger only when the tool is approximately facing the target.
|
||||
Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
|
||||
var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak);
|
||||
bool repair = true;
|
||||
if (angle < MathHelper.PiOver4)
|
||||
{
|
||||
if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i)
|
||||
{
|
||||
var door = i.GetComponent<Door>();
|
||||
// Hit a door, abandon so that we don't weld it shut.
|
||||
return door != null && !door.CanBeTraversed;
|
||||
{
|
||||
if (i.GetComponent<Door>() is Door door && !door.CanBeTraversed )
|
||||
{
|
||||
// Hit a door, don't repair so that we don't weld it shut.
|
||||
if (door.Stuck > 90)
|
||||
{
|
||||
// Almost stuck -> just abandon.
|
||||
return false;
|
||||
}
|
||||
if (door.Stuck > 50)
|
||||
{
|
||||
repair = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check that we don't hit any friendlies
|
||||
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
|
||||
if (repair)
|
||||
{
|
||||
if (hit.UserData is Character c)
|
||||
// Check that we don't hit any friendlies
|
||||
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
|
||||
{
|
||||
if (c == character) { return false; }
|
||||
return HumanAIController.IsFriendly(character, c);
|
||||
if (hit.UserData is Character c)
|
||||
{
|
||||
if (c == character) { return false; }
|
||||
return HumanAIController.IsFriendly(character, c);
|
||||
}
|
||||
return false;
|
||||
}))
|
||||
{
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
Use(deltaTime, character);
|
||||
}
|
||||
return false;
|
||||
}))
|
||||
}
|
||||
repairTimer += deltaTime;
|
||||
if (repairTimer > repairTimeOut)
|
||||
{
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
Use(deltaTime, character);
|
||||
repairTimer += deltaTime;
|
||||
if (repairTimer > repairTimeOut)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow);
|
||||
DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow);
|
||||
#endif
|
||||
Reset();
|
||||
return true;
|
||||
}
|
||||
Reset();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -912,7 +927,7 @@ namespace Barotrauma.Items.Components
|
||||
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
|
||||
foreach (ISerializableEntity target in currentTargets)
|
||||
{
|
||||
if (target is not Door door) { continue; }
|
||||
if (target is not Door door) { continue; }
|
||||
if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; }
|
||||
foreach (var propertyEffect in effect.PropertyEffects)
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
Vector2 DrawSize { get; }
|
||||
|
||||
void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1);
|
||||
void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -111,7 +111,6 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool drawable = true;
|
||||
|
||||
#warning TODO: misnomer - should be IsActiveConditionalLogicalOperator
|
||||
[Serialize(PropertyConditional.LogicalOperatorType.And, IsPropertySaveable.No)]
|
||||
public PropertyConditional.LogicalOperatorType IsActiveConditionalComparison
|
||||
{
|
||||
@@ -159,6 +158,12 @@ namespace Barotrauma.Items.Components
|
||||
protected set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)]
|
||||
public bool LockGuiFramePosition { get; set; }
|
||||
|
||||
[Serialize("0,0", IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)]
|
||||
public Point GuiFrameOffset { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the item be selected by interacting with it.")]
|
||||
public bool CanBeSelected
|
||||
{
|
||||
@@ -251,6 +256,9 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
public float Speed => item.Speed;
|
||||
|
||||
public readonly record struct ItemUseInfo(Item Item, Character User);
|
||||
public readonly NamedEvent<ItemUseInfo> OnUsed = new();
|
||||
|
||||
public readonly bool InheritStatusEffects;
|
||||
|
||||
public ItemComponent(Item item, ContentXElement element)
|
||||
@@ -339,6 +347,7 @@ namespace Barotrauma.Items.Components
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "activeconditional":
|
||||
case "isactiveconditional":
|
||||
case "isactive":
|
||||
IsActiveConditionals ??= new List<PropertyConditional>();
|
||||
IsActiveConditionals.AddRange(PropertyConditional.FromXElement(subElement));
|
||||
@@ -487,7 +496,7 @@ namespace Barotrauma.Items.Components
|
||||
case "trigger_in":
|
||||
if (signal.value != "0")
|
||||
{
|
||||
item.Use(1.0f, signal.sender);
|
||||
item.Use(1.0f, user: signal.sender);
|
||||
}
|
||||
break;
|
||||
case "toggle":
|
||||
@@ -524,7 +533,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
item.ParentInventory.RemoveItem(item);
|
||||
}
|
||||
Entity.Spawner.AddItemToRemoveQueue(item);
|
||||
RemoveItem(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -540,12 +549,23 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
this.Item.ParentInventory.RemoveItem(this.Item);
|
||||
}
|
||||
Entity.Spawner.AddItemToRemoveQueue(this.Item);
|
||||
RemoveItem(this.Item);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Item.Condition += transferAmount;
|
||||
}
|
||||
static void RemoveItem(Item item)
|
||||
{
|
||||
if (Screen.Selected is { IsEditor: true })
|
||||
{
|
||||
item?.Remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
Entity.Spawner?.AddItemToRemoveQueue(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -746,7 +766,7 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard)
|
||||
{
|
||||
if (item.Submarine != null)
|
||||
if (item.Submarine != null && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle)
|
||||
{
|
||||
//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"))
|
||||
@@ -906,7 +926,16 @@ namespace Barotrauma.Items.Components
|
||||
ParseMsg();
|
||||
OverrideRequiredItems(componentElement);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (GuiFrame != null)
|
||||
{
|
||||
GuiFrame.RectTransform.ScreenSpaceOffset = GuiFrameOffset;
|
||||
if (guiFrameDragHandle != null)
|
||||
{
|
||||
guiFrameDragHandle.Enabled = !LockGuiFramePosition;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (item.Submarine != null) { SerializableProperty.UpgradeGameVersion(this, originalElement, item.Submarine.Info.GameVersion); }
|
||||
}
|
||||
|
||||
@@ -922,6 +951,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public virtual void OnScaleChanged() { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the item has an ItemContainer and the contents inside of it changed.
|
||||
/// </summary>
|
||||
public virtual void OnInventoryChanged() { }
|
||||
|
||||
public static ItemComponent Load(ContentXElement element, Item item, bool errorMessages = true)
|
||||
{
|
||||
Type type;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Extensions;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using Barotrauma.Abilities;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
@@ -14,11 +14,11 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition);
|
||||
|
||||
readonly record struct DrawableContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation);
|
||||
readonly record struct ContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation);
|
||||
|
||||
class SlotRestrictions
|
||||
{
|
||||
public readonly int MaxStackSize;
|
||||
public int MaxStackSize;
|
||||
public List<RelatedItem> ContainableItems;
|
||||
public readonly bool AutoInject;
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private readonly List<ActiveContainedItem> activeContainedItems = new List<ActiveContainedItem>();
|
||||
|
||||
private readonly List<DrawableContainedItem> drawableContainedItems = new List<DrawableContainedItem>();
|
||||
private readonly List<ContainedItem> containedItems = new List<ContainedItem>();
|
||||
|
||||
private List<ushort>[] itemIds;
|
||||
|
||||
@@ -128,6 +128,9 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "When this item is equipped, and you 'quick use' (double click / equip button) another equippable item, should the game attempt to move that item inside this one?")]
|
||||
public bool QuickUseMovesItemsInside { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, 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
|
||||
{
|
||||
@@ -141,17 +144,28 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
public bool AccessOnlyWhenBroken { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No)]
|
||||
public bool AllowAccessWhenDropped { get; set; }
|
||||
|
||||
[Serialize(5, IsPropertySaveable.No, description: "How many inventory slots the inventory has per row.")]
|
||||
public int SlotsPerRow { get; set; }
|
||||
|
||||
private readonly HashSet<string> containableRestrictions = new HashSet<string>();
|
||||
private readonly HashSet<Identifier> containableRestrictions = new HashSet<Identifier>();
|
||||
[Editable, Serialize("", IsPropertySaveable.Yes, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")]
|
||||
public string ContainableRestrictions
|
||||
{
|
||||
get { return string.Join(",", containableRestrictions); }
|
||||
set
|
||||
{
|
||||
StringFormatter.ParseCommaSeparatedStringToCollection(value, containableRestrictions);
|
||||
containableRestrictions.Clear();
|
||||
if (!value.IsNullOrEmpty())
|
||||
{
|
||||
foreach (var str in value.Split(','))
|
||||
{
|
||||
if (str.IsNullOrWhiteSpace()) { continue; }
|
||||
containableRestrictions.Add(str.ToIdentifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +211,6 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
public bool RemoveContainedItemsOnDeconstruct { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Can be used by status effects to lock the inventory
|
||||
/// </summary>
|
||||
@@ -207,11 +220,28 @@ namespace Barotrauma.Items.Components
|
||||
set { Inventory.Locked = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used by status effects
|
||||
/// </summary>
|
||||
public int ContainedItemCount
|
||||
{
|
||||
get => Inventory.AllItems.Count();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used by status effects
|
||||
/// </summary>
|
||||
public int ContainedNonBrokenItemCount
|
||||
{
|
||||
get => Inventory.AllItems.Count(it => it.Condition > 0.0f);
|
||||
}
|
||||
|
||||
private readonly ImmutableArray<SlotRestrictions> slotRestrictions;
|
||||
|
||||
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
||||
|
||||
private Vector2 prevContainedItemPositions;
|
||||
private float prevContainedItemRefreshRotation;
|
||||
private Vector2 prevContainedItemRefreshPosition;
|
||||
|
||||
private float autoInjectCooldown = 1.0f;
|
||||
const float AutoInjectInterval = 1.0f;
|
||||
@@ -330,6 +360,16 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
slotRestrictions[i].ContainableItems = ContainableItems;
|
||||
}
|
||||
#if CLIENT
|
||||
if (element.GetChildElement("clearsubcontainerrestrictions") != null)
|
||||
{
|
||||
for (int i = capacity - MainContainerCapacity; i < capacity; i++)
|
||||
{
|
||||
slotRestrictions[i].MaxStackSize = MaxStackSize;
|
||||
slotIcons[i] = null;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public int GetMaxStackSize(int slotIndex)
|
||||
@@ -363,12 +403,31 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
var relatedItem = FindContainableItem(containedItem);
|
||||
drawableContainedItems.RemoveAll(d => d.Item == containedItem);
|
||||
drawableContainedItems.Add(new DrawableContainedItem(containedItem,
|
||||
Hide: relatedItem?.Hide ?? false,
|
||||
ItemPos: relatedItem?.ItemPos,
|
||||
Rotation: relatedItem?.Rotation ?? 0.0f));
|
||||
drawableContainedItems.Sort((DrawableContainedItem it1, DrawableContainedItem it2) => Inventory.FindIndex(it1.Item).CompareTo(Inventory.FindIndex(it2.Item)));
|
||||
var containedItemInfo = new ContainedItem(containedItem,
|
||||
Hide: relatedItem?.Hide ?? false,
|
||||
ItemPos: relatedItem?.ItemPos,
|
||||
Rotation: relatedItem?.Rotation ?? 0.0f);
|
||||
containedItems.RemoveAll(d => d.Item == containedItem);
|
||||
|
||||
if (hideItems)
|
||||
{
|
||||
//if the items aren't visible, the draw order doesn't matter and we can skip the sorting
|
||||
containedItems.Add(containedItemInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
int containedIndex = 0;
|
||||
while (containedIndex < containedItems.Count)
|
||||
{
|
||||
if (index <= Inventory.FindIndex(containedItems[containedIndex].Item))
|
||||
{
|
||||
break;
|
||||
}
|
||||
containedIndex++;
|
||||
}
|
||||
//sort drawables by their order in the inventory
|
||||
containedItems.Insert(containedIndex, containedItemInfo);
|
||||
}
|
||||
|
||||
if (item.GetComponent<Planter>() != null)
|
||||
{
|
||||
@@ -384,6 +443,14 @@ namespace Barotrauma.Items.Components
|
||||
// Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it).
|
||||
SetContainedActive(true);
|
||||
}
|
||||
if (containedItem.FlippedX)
|
||||
{
|
||||
containedItem.FlipX(relativeToSub: false);
|
||||
}
|
||||
if (containedItem.FlippedY)
|
||||
{
|
||||
containedItem.FlipY(relativeToSub: false);
|
||||
}
|
||||
item.SetContainedItemPositions();
|
||||
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
|
||||
OnContainedItemsChanged.Invoke(this);
|
||||
@@ -397,7 +464,8 @@ namespace Barotrauma.Items.Components
|
||||
public void OnItemRemoved(Item containedItem)
|
||||
{
|
||||
activeContainedItems.RemoveAll(i => i.Item == containedItem);
|
||||
drawableContainedItems.RemoveAll(i => i.Item == containedItem);
|
||||
containedItems.RemoveAll(i => i.Item == containedItem);
|
||||
item.SetContainedItemPositions();
|
||||
//deactivate if the inventory is empty
|
||||
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
|
||||
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
|
||||
@@ -406,12 +474,14 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public bool CanBeContained(Item item)
|
||||
{
|
||||
if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; }
|
||||
return slotRestrictions.Any(s => s.MatchesItem(item));
|
||||
}
|
||||
|
||||
public bool CanBeContained(Item item, int index)
|
||||
{
|
||||
if (index < 0 || index >= capacity) { return false; }
|
||||
if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; }
|
||||
return slotRestrictions[index].MatchesItem(item);
|
||||
}
|
||||
|
||||
@@ -463,11 +533,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (item.ParentInventory is CharacterInventory ownerInventory)
|
||||
{
|
||||
if (Vector2.DistanceSquared(prevContainedItemPositions, item.Position) > 10.0f)
|
||||
{
|
||||
SetContainedItemPositions();
|
||||
prevContainedItemPositions = item.Position;
|
||||
}
|
||||
SetContainedItemPositionsIfNeeded();
|
||||
|
||||
if (AutoInject || slotRestrictions.Any(s => s.AutoInject))
|
||||
{
|
||||
@@ -508,11 +574,12 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
}
|
||||
else if (item.body != null &&
|
||||
item.body.Enabled &&
|
||||
item.body.FarseerBody.Awake)
|
||||
else if (item.body != null && item.body.Enabled)
|
||||
{
|
||||
SetContainedItemPositions();
|
||||
if (item.body.FarseerBody.Awake)
|
||||
{
|
||||
SetContainedItemPositionsIfNeeded();
|
||||
}
|
||||
}
|
||||
else if (activeContainedItems.Count == 0)
|
||||
{
|
||||
@@ -553,6 +620,20 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the positions of the contained items if this item has moved/rotated enough
|
||||
/// </summary>
|
||||
private void SetContainedItemPositionsIfNeeded()
|
||||
{
|
||||
if (Vector2.DistanceSquared(prevContainedItemRefreshPosition, item.Position) > 10.0f ||
|
||||
Math.Abs(prevContainedItemRefreshRotation - item.body?.Rotation ?? item.RotationRad) > 0.01f)
|
||||
{
|
||||
SetContainedItemPositions();
|
||||
prevContainedItemRefreshPosition = item.Position;
|
||||
prevContainedItemRefreshRotation = item.body?.Rotation ?? item.RotationRad;
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
{
|
||||
//update when the item is broken too to get OnContaining effects to execute and contained item positions to update
|
||||
@@ -662,10 +743,19 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
SetContainedActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetContainedActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetContainedActive(bool active)
|
||||
{
|
||||
if ((ContainableItems == null || !ContainableItems.Any(c => c.SetActive)) &&
|
||||
(AllSubContainableItems == null || !AllSubContainableItems.Any(c => c.SetActive)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (Item containedItem in Inventory.AllItems)
|
||||
{
|
||||
RelatedItem containableItem = FindContainableItem(containedItem);
|
||||
@@ -722,7 +812,7 @@ namespace Barotrauma.Items.Components
|
||||
case "trigger_in":
|
||||
if (signal.value != "0")
|
||||
{
|
||||
item.Use(1.0f, signal.sender);
|
||||
item.Use(1.0f, user: signal.sender);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -786,7 +876,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
int i = 0;
|
||||
Vector2 currentItemPos = transformedItemPos;
|
||||
foreach (DrawableContainedItem contained in drawableContainedItems)
|
||||
foreach (ContainedItem contained in containedItems)
|
||||
{
|
||||
Vector2 itemPos = currentItemPos;
|
||||
if (contained.ItemPos.HasValue)
|
||||
|
||||
@@ -62,18 +62,58 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public IEnumerable<LimbPos> LimbPositions { get { return limbPositions; } }
|
||||
|
||||
[Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.", alwaysUseInstanceValues: true)]
|
||||
[Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out a signal when interacted with.", alwaysUseInstanceValues: true)]
|
||||
public bool IsToggle
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)]
|
||||
private string output;
|
||||
[ConditionallyEditable(ConditionallyEditable.ConditionType.HasConnectionPanel, onlyInEditors: false),
|
||||
Serialize("1", IsPropertySaveable.Yes, description: "The signal sent when the controller is being activated or is toggled on. If empty, no signal is sent.", alwaysUseInstanceValues: true)]
|
||||
public string Output
|
||||
{
|
||||
get { return output; }
|
||||
set
|
||||
{
|
||||
if (value == null || value == output) { return; }
|
||||
output = value;
|
||||
//reactivate if signal isn't empty (we may not have been previously sending a signal, but might now)
|
||||
if (!value.IsNullOrEmpty()) { IsActive = true; }
|
||||
}
|
||||
}
|
||||
|
||||
private string falseOutput;
|
||||
[ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: false),
|
||||
Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the controller is toggled off. If empty, no signal is sent. Only valid if IsToggle is true.", alwaysUseInstanceValues: true)]
|
||||
public string FalseOutput
|
||||
{
|
||||
get { return falseOutput; }
|
||||
set
|
||||
{
|
||||
if (value == null || value == falseOutput) { return; }
|
||||
falseOutput = value;
|
||||
//reactivate if signal isn't empty (we may not have been previously sending a signal, but might now)
|
||||
if (!value.IsNullOrEmpty()) { IsActive = true; }
|
||||
}
|
||||
}
|
||||
|
||||
private bool state;
|
||||
[ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: true),
|
||||
Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)]
|
||||
public bool State
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get { return state; }
|
||||
set
|
||||
{
|
||||
if (state != value)
|
||||
{
|
||||
state = value;
|
||||
string newOutput = state ? output : falseOutput;
|
||||
IsActive = !string.IsNullOrEmpty(newOutput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Should the HUD (inventory, health bar, etc) be hidden when this item is selected.")]
|
||||
@@ -164,10 +204,11 @@ namespace Barotrauma.Items.Components
|
||||
this.cam = cam;
|
||||
UserInCorrectPosition = false;
|
||||
|
||||
if (IsToggle)
|
||||
string signal = IsToggle && State ? output : falseOutput;
|
||||
if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal))
|
||||
{
|
||||
item.SendSignal(State ? "1" : "0", "signal_out");
|
||||
item.SendSignal(State ? "1" : "0", "trigger_out");
|
||||
item.SendSignal(signal, "signal_out");
|
||||
item.SendSignal(signal, "trigger_out");
|
||||
}
|
||||
|
||||
if (user == null
|
||||
@@ -183,7 +224,7 @@ namespace Barotrauma.Items.Components
|
||||
CancelUsing(user);
|
||||
user = null;
|
||||
}
|
||||
if (!IsToggle || item.Connections == null) { IsActive = false; }
|
||||
if (item.Connections == null || !IsToggle || string.IsNullOrEmpty(signal)) { IsActive = false; }
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -313,9 +354,9 @@ namespace Barotrauma.Items.Components
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (!string.IsNullOrEmpty(output))
|
||||
{
|
||||
item.SendSignal(new Signal("1", sender: user), "trigger_out");
|
||||
item.SendSignal(new Signal(output, sender: user), "trigger_out");
|
||||
}
|
||||
|
||||
lastUsed = Timing.TotalTime;
|
||||
@@ -419,9 +460,9 @@ namespace Barotrauma.Items.Components
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (!string.IsNullOrEmpty(output))
|
||||
{
|
||||
item.SendSignal(new Signal("1", sender: picker), "signal_out");
|
||||
item.SendSignal(new Signal(output, sender: picker), "signal_out");
|
||||
}
|
||||
#if CLIENT
|
||||
PlaySound(ActionType.OnUse, picker);
|
||||
@@ -480,6 +521,7 @@ namespace Barotrauma.Items.Components
|
||||
IsActive = false;
|
||||
CancelUsing(user);
|
||||
user = null;
|
||||
return false;
|
||||
}
|
||||
else if (user.IsBot && !activator.IsBot)
|
||||
{
|
||||
@@ -500,8 +542,11 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
item.SendSignal(new Signal("1", sender: user), "signal_out");
|
||||
#endif
|
||||
if (!string.IsNullOrEmpty(output))
|
||||
{
|
||||
item.SendSignal(new Signal(output, sender: user), "signal_out");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace Barotrauma.Items.Components
|
||||
repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f;
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
|
||||
progressTimer += deltaTime * Math.Min(powerConsumption <= 0.0f ? 1 : Voltage, MaxOverVoltageFactor);
|
||||
|
||||
|
||||
@@ -24,6 +24,14 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
private float targetForce;
|
||||
|
||||
/// <summary>
|
||||
/// Power demand of a marine engine is proportional with the cube of the square root of the thrusting force.
|
||||
/// In practice meaning lower thrust is more effective at conserving power than it would be if the relationship between thrust and power consumption was linear.
|
||||
/// Reverse exponent defined for use with overvoltage calculation: Supplying 2x power will result in 59% more force, 26% more speed, therefore 2x power.
|
||||
/// </summary>
|
||||
private const float ForceToPowerExponent = 3f / 2f;
|
||||
private const float PowerToForceExponent = 1.0f / ForceToPowerExponent;
|
||||
|
||||
private float maxForce;
|
||||
|
||||
private readonly Attack propellerDamage;
|
||||
@@ -130,7 +138,7 @@ namespace Barotrauma.Items.Components
|
||||
if (Math.Abs(Force) > 1.0f)
|
||||
{
|
||||
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor);
|
||||
float currForce = force * voltageFactor;
|
||||
float currForce = force * MathF.Pow(voltageFactor, PowerToForceExponent);
|
||||
float condition = item.MaxCondition <= 0.0f ? 0.0f : item.Condition / item.MaxCondition;
|
||||
// Broken engine makes more noise.
|
||||
float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition);
|
||||
@@ -180,7 +188,7 @@ namespace Barotrauma.Items.Components
|
||||
return 0;
|
||||
}
|
||||
|
||||
currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption;
|
||||
currPowerConsumption = MathF.Pow(Math.Abs(targetForce) / 100.0f, ForceToPowerExponent) * powerConsumption;
|
||||
//engines consume more power when in a bad condition
|
||||
item.GetComponent<Repairable>()?.AdjustPowerConsumption(ref currPowerConsumption);
|
||||
return currPowerConsumption;
|
||||
|
||||
@@ -92,6 +92,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private readonly Dictionary<uint, int> fabricationLimits = new Dictionary<uint, int>();
|
||||
|
||||
public Action<Item, Character> OnItemFabricated;
|
||||
|
||||
public Fabricator(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -128,6 +130,11 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
if (recipeInvalid) { continue; }
|
||||
|
||||
if (fabricationRecipes.TryGetValue(recipe.RecipeHash, out var duplicateRecipe))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\".");
|
||||
continue;
|
||||
}
|
||||
fabricationRecipes.Add(recipe.RecipeHash, recipe);
|
||||
if (recipe.FabricationLimitMax >= 0)
|
||||
{
|
||||
@@ -327,7 +334,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
|
||||
|
||||
float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease;
|
||||
@@ -387,37 +394,35 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (GameMain.NetworkMember is null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
List<Item> foundAvailableItems = new List<Item>();
|
||||
foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems)
|
||||
List<Item> chosenIngredients = new List<Item>();
|
||||
var suitableIngredients = GetSortedSuitableIngredients();
|
||||
|
||||
foreach (var requiredItem in fabricatedItem.RequiredItems)
|
||||
{
|
||||
for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++)
|
||||
for (int i = 0; i < requiredItem.Amount; i++)
|
||||
{
|
||||
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
|
||||
foreach (var suitableIngredient in suitableIngredients)
|
||||
{
|
||||
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
|
||||
if (!requiredItem.MatchesItem(suitableIngredient)) { continue; }
|
||||
if (chosenIngredients.Contains(suitableIngredient)) { continue; }
|
||||
|
||||
var availableItems = availableIngredients[requiredPrefab.Identifier];
|
||||
var availableItem = availableItems.FirstOrDefault(potentialPrefab => requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage));
|
||||
|
||||
if (availableItem == null) { continue; }
|
||||
|
||||
ingredientsStolen |= availableItem.StolenDuringRound;
|
||||
if (!availableItem.AllowStealing)
|
||||
ingredientsStolen |= suitableIngredient.StolenDuringRound;
|
||||
if (!suitableIngredient.AllowStealing)
|
||||
{
|
||||
ingredientsAllowStealing = false;
|
||||
}
|
||||
|
||||
//Leave it behind with reduced condition if it has enough to stay above 0
|
||||
if (requiredItem.UseCondition && availableItem.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f)
|
||||
if (requiredItem.UseCondition && suitableIngredient.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f)
|
||||
{
|
||||
availableItem.Condition -= availableItem.Prefab.Health * requiredItem.MinCondition;
|
||||
suitableIngredient.Condition -= suitableIngredient.Prefab.Health * requiredItem.MinCondition;
|
||||
continue;
|
||||
}
|
||||
if (availableItem.OwnInventory != null)
|
||||
if (suitableIngredient.OwnInventory != null)
|
||||
{
|
||||
foreach (Item containedItem in availableItem.OwnInventory.AllItemsMod)
|
||||
foreach (Item containedItem in suitableIngredient.OwnInventory.AllItemsMod)
|
||||
{
|
||||
if (availableItem.GetComponent<ItemContainer>()?.RemoveContainedItemsOnDeconstruct ?? false)
|
||||
if (suitableIngredient.GetComponent<ItemContainer>()?.RemoveContainedItemsOnDeconstruct ?? false)
|
||||
{
|
||||
Entity.Spawner.AddItemToRemoveQueue(containedItem);
|
||||
}
|
||||
@@ -427,15 +432,13 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foundAvailableItems.Add(availableItem);
|
||||
availableItems.Remove(availableItem);
|
||||
chosenIngredients.Add(suitableIngredient);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fabricationIngredients = new AbilityFabricationItemIngredients(foundAvailableItems);
|
||||
var fabricationIngredients = new AbilityFabricationItemIngredients(chosenIngredients);
|
||||
user?.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients);
|
||||
|
||||
foreach (Item availableItem in fabricationIngredients.Items)
|
||||
@@ -459,7 +462,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount);
|
||||
}
|
||||
user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount);
|
||||
user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount);
|
||||
quality = GetFabricatedItemQuality(fabricatedItem, user);
|
||||
}
|
||||
|
||||
@@ -510,7 +513,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
static void onItemSpawned(Item spawnedItem, Character user)
|
||||
void onItemSpawned(Item spawnedItem, Character user)
|
||||
{
|
||||
if (user != null && user.TeamID != CharacterTeamType.None)
|
||||
{
|
||||
@@ -519,6 +522,7 @@ namespace Barotrauma.Items.Components
|
||||
wifiComponent.TeamID = user.TeamID;
|
||||
}
|
||||
}
|
||||
OnItemFabricated?.Invoke(spawnedItem, user);
|
||||
}
|
||||
if (user?.Info != null && !user.Removed)
|
||||
{
|
||||
@@ -623,6 +627,11 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
if (fabricableItem.HideForNonTraitors)
|
||||
{
|
||||
if (character is not { IsTraitor: true }) { return false; }
|
||||
}
|
||||
|
||||
if (fabricableItem.RequiredMoney > 0)
|
||||
{
|
||||
switch (GameMain.GameSession?.GameMode)
|
||||
@@ -757,6 +766,19 @@ namespace Barotrauma.Items.Components
|
||||
itemList.AddRange(user.Inventory.AllItems);
|
||||
linkedInventories.Add(user.Inventory);
|
||||
}
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
//take materials from characters who've selected a linked container too
|
||||
//(e.g. cabinet that's set to display alongside the fabricator UI)
|
||||
if (c.SelectedItem != null &&
|
||||
c.Inventory != null &&
|
||||
linkedInventories.Contains(c.SelectedItem.OwnInventory) &&
|
||||
!linkedInventories.Contains(c.Inventory))
|
||||
{
|
||||
itemList.AddRange(c.Inventory.AllItems);
|
||||
linkedInventories.Add(c.Inventory);
|
||||
}
|
||||
}
|
||||
availableIngredients.Clear();
|
||||
foreach (Item item in itemList)
|
||||
{
|
||||
@@ -765,34 +787,51 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
availableIngredients[itemIdentifier] = new List<Item>(itemList.Count);
|
||||
}
|
||||
//order by condition (prefer using worst-condition items)
|
||||
int index = 0;
|
||||
while (index < availableIngredients[itemIdentifier].Count &&
|
||||
compare(item, availableIngredients[itemIdentifier][index], inputContainer.Inventory) < 0)
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
static int compare(Item item1, Item item2, Inventory inputInventory)
|
||||
{
|
||||
bool item1InInputInventory = item1.ParentInventory == inputInventory;
|
||||
bool item2InInputInventory = item2.ParentInventory == inputInventory;
|
||||
//prefer items in the input inventory
|
||||
if (item1InInputInventory != item2InInputInventory)
|
||||
{
|
||||
return item1InInputInventory ? 1 : -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
float condition1 = MathUtils.IsValid(item1.Condition) ? item1.Condition : 0;
|
||||
float condition2 = MathUtils.IsValid(item2.Condition) ? item2.Condition : 0;
|
||||
//prefer items in worse condition
|
||||
return Math.Sign(condition2 - condition1);
|
||||
}
|
||||
}
|
||||
|
||||
availableIngredients[itemIdentifier].Insert(index, item);
|
||||
availableIngredients[itemIdentifier].Add(item);
|
||||
}
|
||||
foreach (var itemId in availableIngredients.Keys)
|
||||
{
|
||||
availableIngredients[itemId] = SortIngredients(availableIngredients[itemId]).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Item> SortIngredients(IEnumerable<Item> items)
|
||||
{
|
||||
return items
|
||||
.OrderByDescending(getIngredientContainerPriority)
|
||||
.ThenBy(it => it.Prefab.DefaultPrice?.Price ?? 0)
|
||||
.ThenBy(it => MathUtils.IsValid(it.Condition) ? it.Condition : 0)
|
||||
.ThenByDescending(it => it.ParentInventory?.FindIndex(it) ?? 0);
|
||||
|
||||
int getIngredientContainerPriority(Item item)
|
||||
{
|
||||
if (item.ParentInventory == InputContainer.Inventory)
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
else if (item.ParentInventory is CharacterInventory)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Item> GetSortedSuitableIngredients()
|
||||
{
|
||||
List<Item> suitableIngredients = new List<Item>();
|
||||
foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems)
|
||||
{
|
||||
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
|
||||
{
|
||||
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
|
||||
var availableItems = availableIngredients[requiredPrefab.Identifier];
|
||||
suitableIngredients.AddRange(
|
||||
availableItems.Where(potentialItem => requiredItem.IsConditionSuitable(potentialItem.ConditionPercentage)));
|
||||
}
|
||||
}
|
||||
|
||||
return SortIngredients(suitableIngredients);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -801,43 +840,34 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
private void MoveIngredientsToInputContainer(FabricationRecipe targetItem)
|
||||
{
|
||||
//required ingredients that are already present in the input container
|
||||
List<Item> usedItems = new List<Item>();
|
||||
List<Item> chosenIngredients = new List<Item>();
|
||||
var suitableIngredients = GetSortedSuitableIngredients();
|
||||
|
||||
targetItem.RequiredItems.ForEach(requiredItem => {
|
||||
foreach (var requiredItem in targetItem.RequiredItems)
|
||||
{
|
||||
for (int i = 0; i < requiredItem.Amount; i++)
|
||||
{
|
||||
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
|
||||
foreach (var suitableIngredient in suitableIngredients)
|
||||
{
|
||||
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
|
||||
if (!requiredItem.MatchesItem(suitableIngredient)) { continue; }
|
||||
if (chosenIngredients.Contains(suitableIngredient)) { continue; }
|
||||
|
||||
var availablePrefabs = availableIngredients[requiredPrefab.Identifier];
|
||||
var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab =>
|
||||
//in another inventory, we need to move the item
|
||||
if (suitableIngredient.ParentInventory != inputContainer.Inventory)
|
||||
{
|
||||
return !usedItems.Contains(potentialPrefab) && requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage);
|
||||
});
|
||||
if (availablePrefab == null) { continue; }
|
||||
|
||||
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))
|
||||
if (!inputContainer.Inventory.CanBePut(suitableIngredient))
|
||||
{
|
||||
var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it));
|
||||
var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !chosenIngredients.Contains(it));
|
||||
unneededItem?.Drop(null);
|
||||
}
|
||||
inputContainer.Inventory.TryPutItem(availablePrefab, user: null);
|
||||
inputContainer.Inventory.TryPutItem(suitableIngredient, user: null);
|
||||
}
|
||||
chosenIngredients.Add(suitableIngredient);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RefreshAvailableIngredients();
|
||||
}
|
||||
|
||||
@@ -884,6 +914,13 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
savedFabricatedItem = null;
|
||||
}
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
OnItemFabricated = null;
|
||||
}
|
||||
|
||||
class AbilityFabricatorSkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier
|
||||
{
|
||||
public AbilityFabricatorSkillGain(Identifier skillIdentifier, float skillAmount)
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace Barotrauma.Items.Components
|
||||
hasPower = Voltage > MinVoltage;
|
||||
if (hasPower)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
UpdateProjSpecific(deltaTime);
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
|
||||
if (item.CurrentHull == null) { return; }
|
||||
|
||||
|
||||
@@ -203,6 +203,8 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
public bool MeltedDownThisRound { get; private set; }
|
||||
|
||||
public Reactor(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -282,7 +284,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
prevAvailableFuel = AvailableFuel;
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
|
||||
//use a smoothed "correct output" instead of the actual correct output based on the load
|
||||
//so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily
|
||||
@@ -338,7 +340,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
foreach (Item item in containedItems)
|
||||
{
|
||||
if (!item.HasTag("reactorfuel")) { continue; }
|
||||
if (!item.HasTag(Tags.Fuel)) { continue; }
|
||||
if (fissionRate > 0.0f)
|
||||
{
|
||||
bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost &&
|
||||
@@ -648,6 +650,7 @@ namespace Barotrauma.Items.Components
|
||||
item.Condition = 0.0f;
|
||||
fireTimer = 0.0f;
|
||||
meltDownTimer = 0.0f;
|
||||
MeltedDownThisRound = true;
|
||||
|
||||
var containedItems = item.OwnInventory?.AllItems;
|
||||
if (containedItems != null)
|
||||
@@ -704,13 +707,13 @@ namespace Barotrauma.Items.Components
|
||||
var containObjective = AIContainItems<Reactor>(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true);
|
||||
containObjective.Completed += ReportFuelRodCount;
|
||||
containObjective.Abandoned += ReportFuelRodCount;
|
||||
character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, "reactorfuel".ToIdentifier(), 30.0f);
|
||||
character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.Fuel, 30.0f);
|
||||
|
||||
void ReportFuelRodCount()
|
||||
{
|
||||
if (!character.IsOnPlayerTeam) { return; }
|
||||
if (character.Submarine != Submarine.MainSub) { return; }
|
||||
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1);
|
||||
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.Fuel) && i.Condition > 1);
|
||||
if (remainingFuelRods == 0)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogOutOfFuelRods").Value, null, 0.0f, "outoffuelrods".ToIdentifier(), 30.0f);
|
||||
|
||||
@@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components
|
||||
user = null;
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
|
||||
float userSkill = 0.0f;
|
||||
if (user != null && controlledSub != null &&
|
||||
@@ -508,7 +508,7 @@ namespace Barotrauma.Items.Components
|
||||
var closeCells = Level.Loaded.GetCells(controlledSub.WorldPosition, 4);
|
||||
foreach (VoronoiCell cell in closeCells)
|
||||
{
|
||||
if (cell.DoesDamage)
|
||||
if (cell.DoesDamage || cell.Body is { BodyType: BodyType.Dynamic })
|
||||
{
|
||||
foreach (GraphEdge edge in cell.Edges)
|
||||
{
|
||||
@@ -525,7 +525,7 @@ namespace Barotrauma.Items.Components
|
||||
newAvoidStrength += avoid;
|
||||
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, edge.Center, 1.0f, avoid, cell.Translation));
|
||||
|
||||
if (dot > 0.0f)
|
||||
if (dot > 0.0f && cell.DoesDamage)
|
||||
{
|
||||
showIceSpireWarning = true;
|
||||
}
|
||||
|
||||
@@ -138,6 +138,8 @@ namespace Barotrauma.Items.Components
|
||||
set { flipIndicator = value; }
|
||||
}
|
||||
|
||||
public bool OutputDisabled { get; private set; }
|
||||
|
||||
public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed;
|
||||
|
||||
public const float aiRechargeTargetRatio = 0.5f;
|
||||
@@ -162,19 +164,19 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
adjustedCapacity = GetCapacity();
|
||||
if (item.Connections == null)
|
||||
{
|
||||
IsActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
adjustedCapacity = GetCapacity();
|
||||
isRunning = true;
|
||||
float chargeRatio = charge / adjustedCapacity;
|
||||
|
||||
if (chargeRatio > 0.0f)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
}
|
||||
|
||||
float loadReading = 0;
|
||||
@@ -242,6 +244,7 @@ namespace Barotrauma.Items.Components
|
||||
/// <returns>Minimum and maximum power output for the connection</returns>
|
||||
public override PowerRange MinMaxPowerOut(Connection connection, float load = 0)
|
||||
{
|
||||
if (OutputDisabled) { return PowerRange.Zero; }
|
||||
if (connection == powerOut)
|
||||
{
|
||||
float maxOutput;
|
||||
@@ -275,6 +278,7 @@ namespace Barotrauma.Items.Components
|
||||
/// <returns></returns>
|
||||
public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load)
|
||||
{
|
||||
if (OutputDisabled) { return 0; }
|
||||
//Only power out connection can provide power and Max poweroutput can't be negative
|
||||
if (connection == powerOut && minMaxPower.Max > 0)
|
||||
{
|
||||
@@ -367,22 +371,27 @@ namespace Barotrauma.Items.Components
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
if (connection.IsPower) { return; }
|
||||
|
||||
if (connection.Name == "set_rate")
|
||||
switch (connection.Name)
|
||||
{
|
||||
if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed))
|
||||
{
|
||||
if (!MathUtils.IsValid(tempSpeed)) { return; }
|
||||
|
||||
float rechargeRate = MathHelper.Clamp(tempSpeed / 100.0f, 0.0f, 1.0f);
|
||||
RechargeSpeed = rechargeRate * MaxRechargeSpeed;
|
||||
#if CLIENT
|
||||
if (rechargeSpeedSlider != null)
|
||||
case "disable_output":
|
||||
OutputDisabled = signal.value != "0";
|
||||
break;
|
||||
case "set_rate":
|
||||
if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed))
|
||||
{
|
||||
rechargeSpeedSlider.BarScroll = rechargeRate;
|
||||
}
|
||||
if (!MathUtils.IsValid(tempSpeed)) { return; }
|
||||
|
||||
float rechargeRate = MathHelper.Clamp(tempSpeed / 100.0f, 0.0f, 1.0f);
|
||||
RechargeSpeed = rechargeRate * MaxRechargeSpeed;
|
||||
#if CLIENT
|
||||
if (rechargeSpeedSlider != null)
|
||||
{
|
||||
rechargeSpeedSlider.BarScroll = rechargeRate;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ namespace Barotrauma.Items.Components
|
||||
isBroken = false;
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
|
||||
float powerReadingOut = 0;
|
||||
float loadReadingOut = ExtraLoad;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
#if CLIENT
|
||||
using Barotrauma.Sounds;
|
||||
#endif
|
||||
@@ -193,13 +194,13 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
//if the item consumes no power, ignore the voltage requirement and
|
||||
//apply OnActive statuseffects as long as this component is active
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Voltage > minVoltage)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
}
|
||||
#if CLIENT
|
||||
if (Voltage > minVoltage)
|
||||
@@ -666,7 +667,7 @@ namespace Barotrauma.Items.Components
|
||||
return
|
||||
conn1.IsPower && conn2.IsPower &&
|
||||
conn1.Item.Condition > 0.0f && conn2.Item.Condition > 0.0f &&
|
||||
(conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.Item.HasTag("dock") || conn2.Item.HasTag("dock") || conn1.IsOutput != conn2.IsOutput);
|
||||
(conn1.Item.HasTag(Tags.JunctionBox) || conn2.Item.HasTag(Tags.JunctionBox) || conn1.Item.HasTag(Tags.DockingPort) || conn2.Item.HasTag(Tags.DockingPort) || conn1.IsOutput != conn2.IsOutput);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -682,6 +683,7 @@ namespace Barotrauma.Items.Components
|
||||
if (!recipient.IsPower || !recipient.IsOutput) { continue; }
|
||||
var battery = recipient.Item?.GetComponent<PowerContainer>();
|
||||
if (battery == null || battery.Item.Condition <= 0.0f) { continue; }
|
||||
if (battery.OutputDisabled) { continue; }
|
||||
float maxOutputPerFrame = battery.MaxOutPut / 60.0f;
|
||||
float framesPerMinute = 3600.0f;
|
||||
availablePower += Math.Min(battery.Charge * framesPerMinute, maxOutputPerFrame);
|
||||
@@ -689,23 +691,19 @@ namespace Barotrauma.Items.Components
|
||||
return availablePower;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of batteries directly connected to the item
|
||||
/// </summary>
|
||||
protected List<PowerContainer> GetDirectlyConnectedBatteries()
|
||||
protected IEnumerable<PowerContainer> GetDirectlyConnectedBatteries()
|
||||
{
|
||||
List<PowerContainer> batteries = new List<PowerContainer>();
|
||||
if (item.Connections == null || powerIn == null) { return batteries; }
|
||||
foreach (Connection recipient in powerIn.Recipients)
|
||||
if (item.Connections != null && powerIn != null)
|
||||
{
|
||||
if (!recipient.IsPower || !recipient.IsOutput) { continue; }
|
||||
var battery = recipient.Item?.GetComponent<PowerContainer>();
|
||||
if (battery != null)
|
||||
foreach (Connection recipient in powerIn.Recipients)
|
||||
{
|
||||
batteries.Add(battery);
|
||||
if (!recipient.IsPower || !recipient.IsOutput) { continue; }
|
||||
if (recipient.Item?.GetComponent<PowerContainer>() is PowerContainer battery)
|
||||
{
|
||||
yield return battery;
|
||||
}
|
||||
}
|
||||
}
|
||||
return batteries;
|
||||
}
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
|
||||
@@ -56,6 +56,16 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
enum StickTargetType
|
||||
{
|
||||
Structure,
|
||||
Limb,
|
||||
Item,
|
||||
Submarine,
|
||||
LevelWall,
|
||||
Unknown
|
||||
}
|
||||
|
||||
public const float WaterDragCoefficient = 0.1f;
|
||||
|
||||
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
|
||||
@@ -353,7 +363,7 @@ namespace Barotrauma.Items.Components
|
||||
if (Item.Removed) { return; }
|
||||
launchPos = simPosition;
|
||||
//set the rotation of the projectile again because dropping the projectile resets the rotation
|
||||
Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians));
|
||||
Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians), findNewHull: false);
|
||||
if (DeactivationTime > 0)
|
||||
{
|
||||
deactivationTimer = DeactivationTime;
|
||||
@@ -442,27 +452,49 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
hits.Clear();
|
||||
|
||||
Vector2 prevVelocity = item.body.LinearVelocity;
|
||||
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
|
||||
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
|
||||
}
|
||||
|
||||
//do not create a network event about dropping the projectile at this point,
|
||||
//otherwise clients would fail to launch the correct projectile when firing the weapon
|
||||
var prevInventory = item.ParentInventory;
|
||||
if (prevInventory != null && GameMain.NetworkMember is { IsServer: true })
|
||||
{
|
||||
//update the state of the inventory after a delay,
|
||||
//in case a client failed to launch the projectile for some reason
|
||||
CoroutineManager.Invoke(() =>
|
||||
{
|
||||
if (item.Removed) { return; }
|
||||
prevInventory.CreateNetworkEvent();
|
||||
}, delay: CorrectionDelay / 2);
|
||||
}
|
||||
item.Drop(null, createNetworkEvent: false);
|
||||
|
||||
Item.WaterDragCoefficient = WaterDragCoefficient;
|
||||
|
||||
launchPos = item.SimPosition;
|
||||
|
||||
item.body.Enabled = true;
|
||||
item.body.Enabled = true;
|
||||
if (item.body.BodyType == BodyType.Kinematic)
|
||||
{
|
||||
item.body.LinearVelocity = impulse;
|
||||
}
|
||||
else
|
||||
else if (impulse.LengthSquared() > 0.001f)
|
||||
{
|
||||
impulse *= item.body.Mass;
|
||||
item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f);
|
||||
}
|
||||
else
|
||||
{
|
||||
//if no impulse is defined, maintain the projectile's original velocity (if any)
|
||||
//can be used to make throwable items behave as projectiles
|
||||
item.body.LinearVelocity = prevVelocity;
|
||||
}
|
||||
|
||||
item.body.FarseerBody.OnCollision += OnProjectileCollision;
|
||||
item.body.FarseerBody.IsBullet = true;
|
||||
@@ -483,7 +515,7 @@ namespace Barotrauma.Items.Components
|
||||
float rotation = item.body.Rotation;
|
||||
Vector2 simPositon = item.SimPosition;
|
||||
Vector2 rayStartWorld = item.WorldPosition;
|
||||
item.Drop(null);
|
||||
item.Drop(null, createNetworkEvent: false);
|
||||
Item.WaterDragCoefficient = WaterDragCoefficient;
|
||||
|
||||
item.body.Enabled = true;
|
||||
@@ -666,6 +698,7 @@ namespace Barotrauma.Items.Components
|
||||
if (fixture.Body.UserData is VoronoiCell) { return -1; }
|
||||
if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; }
|
||||
if (fixture.Body.UserData is Limb limb && limb.character?.Submarine != submarine) { return -1; }
|
||||
if (fixture.Body == Level.Loaded?.TopBarrier || fixture.Body == Level.Loaded?.BottomBarrier) { return -1; }
|
||||
}
|
||||
|
||||
// Ignore holdables that can't push -> shouldn't block
|
||||
@@ -891,8 +924,17 @@ namespace Barotrauma.Items.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ?
|
||||
contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity);
|
||||
Vector2 normalizedVel;
|
||||
Vector2 dir;
|
||||
if (item.body.LinearVelocity.LengthSquared() < 0.001f)
|
||||
{
|
||||
normalizedVel = Vector2.Zero;
|
||||
dir = contact.Manifold.LocalNormal;
|
||||
}
|
||||
else
|
||||
{
|
||||
normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity);
|
||||
}
|
||||
|
||||
//do a raycast in the sub's coordinate space to see if it hit a structure
|
||||
var wallBody = Submarine.PickBody(
|
||||
@@ -901,7 +943,7 @@ namespace Barotrauma.Items.Components
|
||||
collisionCategory: Physics.CollisionWall);
|
||||
if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) &&
|
||||
//ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction
|
||||
Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0)
|
||||
Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0)
|
||||
{
|
||||
target = wallBody.FixtureList.First();
|
||||
if (hits.Contains(target.Body))
|
||||
@@ -1136,7 +1178,13 @@ namespace Barotrauma.Items.Components
|
||||
removePending = true;
|
||||
item.HiddenInGame = true;
|
||||
item.body.FarseerBody.Enabled = false;
|
||||
Entity.Spawner?.AddItemToRemoveQueue(item);
|
||||
//delete with a brief delay so we don't delete the item
|
||||
//before a client has managed to launch it (can happen with hitscan projectile)
|
||||
CoroutineManager.Invoke(() =>
|
||||
{
|
||||
if (item.Removed) { return; }
|
||||
Entity.Spawner?.AddItemToRemoveQueue(item);
|
||||
}, delay: 0.5f);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1181,6 +1229,11 @@ namespace Barotrauma.Items.Components
|
||||
IgnoredBodies?.Clear();
|
||||
}
|
||||
|
||||
public bool IsAttachedTo(PhysicsBody body)
|
||||
{
|
||||
return stickJoint != null && (stickJoint.BodyA == body?.FarseerBody || stickJoint.BodyB == body?.FarseerBody);
|
||||
}
|
||||
|
||||
private void StickToTarget(Body targetBody, Vector2 axis)
|
||||
{
|
||||
if (stickJoint != null) { return; }
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class RemoteController : ItemComponent
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.No, description: "Tag or identifier of the item that should be controlled.")]
|
||||
public string Target
|
||||
public Identifier Target
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
|
||||
@@ -14,7 +14,11 @@ namespace Barotrauma.Items.Components
|
||||
private readonly LocalizedString header;
|
||||
|
||||
private float deteriorationTimer;
|
||||
private float deteriorateAlwaysResetTimer;
|
||||
public float ForceDeteriorationTimer
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private int updateDeteriorationCounter;
|
||||
private const int UpdateDeteriorationInterval = 10;
|
||||
@@ -62,6 +66,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(60f, IsPropertySaveable.Yes, description: "How long will the item spontaneously deteriorate after being sabotaged.")]
|
||||
public float SabotageDeteriorationDuration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(80.0f, IsPropertySaveable.Yes, description: "The condition of the item has to be below this for it to become repairable. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
|
||||
public float RepairThreshold
|
||||
{
|
||||
@@ -83,13 +94,6 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "If set to true, the deterioration timer will always run regardless if the item is being used or not.")]
|
||||
public bool DeteriorateAlways
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private float skillRequirementMultiplier;
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes)]
|
||||
@@ -394,20 +398,19 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
item.SendSignal(conditionSignal, "condition_out");
|
||||
|
||||
if (ForceDeteriorationTimer > 0.0f)
|
||||
{
|
||||
ForceDeteriorationTimer -= deltaTime;
|
||||
if (ForceDeteriorationTimer <= 0.0f)
|
||||
{
|
||||
#if SERVER
|
||||
//let the clients know the deterioration delay
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (CurrentFixer == null)
|
||||
{
|
||||
if (deteriorateAlwaysResetTimer > 0.0f)
|
||||
{
|
||||
deteriorateAlwaysResetTimer -= deltaTime;
|
||||
if (deteriorateAlwaysResetTimer <= 0.0f)
|
||||
{
|
||||
DeteriorateAlways = false;
|
||||
#if SERVER
|
||||
//let the clients know the deterioration delay
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
updateDeteriorationCounter++;
|
||||
if (updateDeteriorationCounter >= UpdateDeteriorationInterval)
|
||||
{
|
||||
@@ -534,8 +537,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
deteriorationTimer = 0.0f;
|
||||
deteriorateAlwaysResetTimer = item.Condition / DeteriorationSpeed;
|
||||
DeteriorateAlways = true;
|
||||
ForceDeteriorationTimer = SabotageDeteriorationDuration;
|
||||
item.Condition = item.MaxCondition * (MinSabotageCondition / 100);
|
||||
wasGoodCondition = false;
|
||||
}
|
||||
@@ -553,7 +555,8 @@ namespace Barotrauma.Items.Components
|
||||
if (item.Condition <= 0.0f) { return; }
|
||||
if (!ShouldDeteriorate()) { return; }
|
||||
|
||||
if (deteriorationTimer > 0.0f)
|
||||
//forced deterioration doesn't tick down the timer for spontaneous deterioration
|
||||
if (deteriorationTimer > 0.0f && ForceDeteriorationTimer <= 0.0f)
|
||||
{
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
@@ -568,6 +571,7 @@ namespace Barotrauma.Items.Components
|
||||
if (item.ConditionPercentage > MinDeteriorationCondition)
|
||||
{
|
||||
float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
|
||||
if (ForceDeteriorationTimer > 0.0f) { deteriorationSpeed = Math.Max(deteriorationSpeed, 1.0f); }
|
||||
item.Condition -= deteriorationSpeed * deltaTime;
|
||||
}
|
||||
}
|
||||
@@ -592,7 +596,7 @@ namespace Barotrauma.Items.Components
|
||||
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 (item.HasTag(Tags.TurretAmmoSource)) { return true; }
|
||||
if (!character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors)) { return false; }
|
||||
if (item.GetComponent<Fabricator>() != null) { return true; }
|
||||
if (item.GetComponent<Deconstructor>() != null) { return true; }
|
||||
@@ -623,6 +627,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool ShouldDeteriorate()
|
||||
{
|
||||
if (ForceDeteriorationTimer > 0.0f) { return true; }
|
||||
|
||||
if (Level.IsLoadedFriendlyOutpost) { return false; }
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession?.GameMode is TutorialMode) { return false; }
|
||||
@@ -666,13 +672,13 @@ namespace Barotrauma.Items.Components
|
||||
//oxygen generators don't deteriorate if they're not running
|
||||
if (oxyGenerator.CurrFlow > 0.1f) { return true; }
|
||||
}
|
||||
else if (ic is Powered powered && !(powered is LightComponent))
|
||||
else if (ic is Powered powered && powered is not LightComponent)
|
||||
{
|
||||
if (powered.Voltage >= powered.MinVoltage) { return true; }
|
||||
}
|
||||
}
|
||||
|
||||
return DeteriorateAlways;
|
||||
return false;
|
||||
}
|
||||
|
||||
private float GetDeteriorationDelayMultiplier()
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 diff = target.WorldPosition - source.WorldPosition;
|
||||
Vector2 diff = target.WorldPosition - GetSourcePos(useDrawPosition: false);
|
||||
float lengthSqr = diff.LengthSquared();
|
||||
if (lengthSqr > MaxLength * MaxLength)
|
||||
{
|
||||
@@ -372,7 +372,40 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private PhysicsBody GetBodyToPull(ISpatialEntity target)
|
||||
/// <summary>
|
||||
/// Get the position the rope starts from (taking into account barrel positions if needed)
|
||||
/// </summary>
|
||||
/// <param name="useDrawPosition">Should the interpolated draw position be used? If not, the WorldPosition is used.</param>
|
||||
private Vector2 GetSourcePos(bool useDrawPosition = false)
|
||||
{
|
||||
Vector2 sourcePos = source.WorldPosition;
|
||||
if (source is Item sourceItem)
|
||||
{
|
||||
if (useDrawPosition)
|
||||
{
|
||||
sourcePos = sourceItem.DrawPosition;
|
||||
}
|
||||
if (!sourceItem.Removed)
|
||||
{
|
||||
if (sourceItem.GetComponent<Turret>() is { } turret)
|
||||
{
|
||||
sourcePos = new Vector2(sourceItem.WorldRect.X + turret.TransformedBarrelPos.X, sourceItem.WorldRect.Y - turret.TransformedBarrelPos.Y);
|
||||
}
|
||||
else if (sourceItem.GetComponent<RangedWeapon>() is { } weapon)
|
||||
{
|
||||
sourcePos += ConvertUnits.ToDisplayUnits(weapon.TransformedBarrelPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (useDrawPosition && source is Limb sourceLimb && sourceLimb.body != null)
|
||||
{
|
||||
sourcePos = sourceLimb.body.DrawPosition;
|
||||
}
|
||||
return sourcePos;
|
||||
}
|
||||
|
||||
|
||||
private static PhysicsBody GetBodyToPull(ISpatialEntity target)
|
||||
{
|
||||
if (target is Item targetItem)
|
||||
{
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
[InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)]
|
||||
[InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)]
|
||||
public string FalseOutput
|
||||
{
|
||||
get { return falseOutput; }
|
||||
|
||||
@@ -0,0 +1,684 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
internal sealed partial class CircuitBox : ItemComponent, IClientSerializable, IServerSerializable
|
||||
{
|
||||
public static readonly ImmutableHashSet<CircuitBoxOpcode> UnrealiableOpcodes
|
||||
= ImmutableHashSet.Create(CircuitBoxOpcode.Cursor);
|
||||
|
||||
public ImmutableArray<CircuitBoxInputConnection> Inputs;
|
||||
public ImmutableArray<CircuitBoxOutputConnection> Outputs;
|
||||
|
||||
public readonly List<CircuitBoxComponent> Components = new List<CircuitBoxComponent>();
|
||||
|
||||
public readonly List<CircuitBoxInputOutputNode> InputOutputNodes = new();
|
||||
|
||||
public readonly List<CircuitBoxWire> Wires = new List<CircuitBoxWire>();
|
||||
|
||||
public override bool IsActive => true;
|
||||
|
||||
public Option<CircuitBoxConnection> FindInputOutputConnection(Identifier connectionName)
|
||||
{
|
||||
foreach (CircuitBoxInputConnection input in Inputs)
|
||||
{
|
||||
if (input.Name != connectionName) { continue; }
|
||||
|
||||
return Option.Some<CircuitBoxConnection>(input);
|
||||
}
|
||||
|
||||
foreach (CircuitBoxOutputConnection output in Outputs)
|
||||
{
|
||||
if (output.Name != connectionName) { continue; }
|
||||
|
||||
return Option.Some<CircuitBoxConnection>(output);
|
||||
}
|
||||
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
public readonly ItemContainer[] containers;
|
||||
|
||||
private const int ComponentContainerIndex = 0,
|
||||
WireContainerIndex = 1;
|
||||
|
||||
public ItemContainer? ComponentContainer
|
||||
=> GetContainerOrNull(ComponentContainerIndex);
|
||||
|
||||
// wire container falls back to the main container if one isn't specified
|
||||
public ItemContainer? WireContainer
|
||||
=> GetContainerOrNull(WireContainerIndex) ?? GetContainerOrNull(ComponentContainerIndex);
|
||||
|
||||
public bool IsFull => ComponentContainer?.Inventory is { } inventory && inventory.IsFull(true);
|
||||
|
||||
public CircuitBox(Item item, ContentXElement element) : base(item, element)
|
||||
{
|
||||
containers = item.GetComponents<ItemContainer>().ToArray();
|
||||
if (containers.Length < 1)
|
||||
{
|
||||
DebugConsole.ThrowError("Circuit box must have at least one item container to function.");
|
||||
}
|
||||
|
||||
InitProjSpecific(element);
|
||||
|
||||
var inputBuilder = ImmutableArray.CreateBuilder<CircuitBoxInputConnection>();
|
||||
var outputBuilder = ImmutableArray.CreateBuilder<CircuitBoxOutputConnection>();
|
||||
|
||||
foreach (Connection conn in Item.Connections)
|
||||
{
|
||||
if (conn.IsOutput)
|
||||
{
|
||||
outputBuilder.Add(new CircuitBoxOutputConnection(Vector2.Zero, conn, this));
|
||||
}
|
||||
else
|
||||
{
|
||||
inputBuilder.Add(new CircuitBoxInputConnection(Vector2.Zero, conn, this));
|
||||
}
|
||||
}
|
||||
|
||||
Inputs = inputBuilder.ToImmutable();
|
||||
Outputs = outputBuilder.ToImmutable();
|
||||
|
||||
InputOutputNodes.Add(new CircuitBoxInputOutputNode(Inputs, new Vector2(-512, 0f), CircuitBoxInputOutputNode.Type.Input, this));
|
||||
InputOutputNodes.Add(new CircuitBoxInputOutputNode(Outputs, new Vector2(512, 0f), CircuitBoxInputOutputNode.Type.Output, this));
|
||||
|
||||
item.OnDeselect += OnDeselected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We want to load the components after the map has loaded since we need to link up the components to their items
|
||||
/// and pretty much all items have higher ID than the circuit box.
|
||||
/// </summary>
|
||||
private Option<ContentXElement> delayedElementToLoad;
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
if (delayedElementToLoad.IsSome()) { return; }
|
||||
delayedElementToLoad = Option.Some(componentElement);
|
||||
}
|
||||
|
||||
public override void OnInventoryChanged()
|
||||
=> OnViewUpdateProjSpecific();
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
#if CLIENT
|
||||
// When loading from the server the wires cannot be properly loaded and connected up because we might not be loaded in properly yet.
|
||||
// So we need to wait until the circuit box starts updating and then we can ensure the wires are connected.
|
||||
if (wasInitializedByServer)
|
||||
{
|
||||
foreach (var w in Wires)
|
||||
{
|
||||
w.EnsureWireConnected();
|
||||
}
|
||||
wasInitializedByServer = false;
|
||||
}
|
||||
#endif
|
||||
TryInitializeNodes();
|
||||
}
|
||||
|
||||
public override void OnMapLoaded()
|
||||
=> TryInitializeNodes();
|
||||
|
||||
private void TryInitializeNodes()
|
||||
{
|
||||
if (!delayedElementToLoad.TryUnwrap(out var loadElement)) { return; }
|
||||
LoadFromXML(loadElement);
|
||||
delayedElementToLoad = Option.None;
|
||||
}
|
||||
|
||||
private void LoadFromXML(ContentXElement loadElement)
|
||||
{
|
||||
foreach (var subElement in loadElement.Elements())
|
||||
{
|
||||
string elementName = subElement.Name.ToString().ToLowerInvariant();
|
||||
switch (elementName)
|
||||
{
|
||||
case "component" when CircuitBoxComponent.TryLoadFromXML(subElement, this).TryUnwrap(out var comp):
|
||||
Components.Add(comp);
|
||||
break;
|
||||
case "wire" when CircuitBoxWire.TryLoadFromXML(subElement, this).TryUnwrap(out var wire):
|
||||
Wires.Add(wire);
|
||||
break;
|
||||
case "inputnode":
|
||||
LoadFor(CircuitBoxInputOutputNode.Type.Input, subElement);
|
||||
break;
|
||||
case "outputnode":
|
||||
LoadFor(CircuitBoxInputOutputNode.Type.Output, subElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
// We need to let the clients know of the loaded data
|
||||
if (needsServerInitialization)
|
||||
{
|
||||
CreateInitializationEvent();
|
||||
needsServerInitialization = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void LoadFor(CircuitBoxInputOutputNode.Type type, ContentXElement subElement)
|
||||
{
|
||||
foreach (var node in InputOutputNodes)
|
||||
{
|
||||
if (node.NodeType != type) { continue; }
|
||||
|
||||
node.Load(subElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CloneFrom(CircuitBox original, Dictionary<ushort, Item> clonedContainedItems)
|
||||
{
|
||||
Components.Clear();
|
||||
Wires.Clear();
|
||||
|
||||
foreach (var origComp in original.Components)
|
||||
{
|
||||
var newComponent = new CircuitBoxComponent(origComp.ID, clonedContainedItems[origComp.Item.ID], origComp.Position, this, origComp.UsedResource);
|
||||
Components.Add(newComponent);
|
||||
}
|
||||
|
||||
for (int ioIndex = 0; ioIndex < original.InputOutputNodes.Count; ioIndex++)
|
||||
{
|
||||
var origNode = original.InputOutputNodes[ioIndex];
|
||||
var cloneNode = InputOutputNodes[ioIndex];
|
||||
|
||||
cloneNode.Position = origNode.Position;
|
||||
}
|
||||
|
||||
foreach (var origWire in original.Wires)
|
||||
{
|
||||
Option<CircuitBoxConnection> to = CircuitBoxConnectorIdentifier.FromConnection(origWire.To).FindConnection(this),
|
||||
from = CircuitBoxConnectorIdentifier.FromConnection(origWire.From).FindConnection(this);
|
||||
|
||||
if (!to.TryUnwrap(out var toConn) || !from.TryUnwrap(out var fromConn))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error while cloning item \"{Name}\" - failed to find a connection for a wire. ");
|
||||
continue;
|
||||
}
|
||||
|
||||
var newWire = new CircuitBoxWire(this, origWire.ID, origWire.BackingWire.Select(w => clonedContainedItems[w.ID]), fromConn, toConn, origWire.UsedItemPrefab);
|
||||
Wires.Add(newWire);
|
||||
}
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
XElement componentElement = base.Save(parentElement);
|
||||
|
||||
foreach (CircuitBoxInputOutputNode node in InputOutputNodes)
|
||||
{
|
||||
componentElement.Add(node.Save());
|
||||
}
|
||||
|
||||
foreach (CircuitBoxComponent node in Components)
|
||||
{
|
||||
componentElement.Add(node.Save());
|
||||
}
|
||||
|
||||
foreach (CircuitBoxWire wire in Wires)
|
||||
{
|
||||
componentElement.Add(wire.Save());
|
||||
}
|
||||
|
||||
return componentElement;
|
||||
}
|
||||
|
||||
public partial void OnDeselected(Character c);
|
||||
|
||||
public record struct CreatedWire(CircuitBoxConnectorIdentifier Start, CircuitBoxConnectorIdentifier End, Option<Item> Item, ushort ID);
|
||||
|
||||
public bool Connect(CircuitBoxConnection one, CircuitBoxConnection two, Action<CreatedWire> onCreated, ItemPrefab selectedWirePrefab)
|
||||
{
|
||||
if (!VerifyConnection(one, two)) { return false; }
|
||||
|
||||
ushort id = ICircuitBoxIdentifiable.FindFreeID(Wires);
|
||||
switch (one.IsOutput)
|
||||
{
|
||||
case true when !two.IsOutput:
|
||||
{
|
||||
CircuitBoxConnectorIdentifier start = CircuitBoxConnectorIdentifier.FromConnection(one),
|
||||
end = CircuitBoxConnectorIdentifier.FromConnection(two);
|
||||
|
||||
if (IsExternalConnection(one) || IsExternalConnection(two))
|
||||
{
|
||||
CreateWireWithoutItem(one, two, id, selectedWirePrefab);
|
||||
onCreated(new CreatedWire(start, end, Option.None, id));
|
||||
return true;
|
||||
}
|
||||
|
||||
CreateWireWithItem(one, two, selectedWirePrefab, id, i => onCreated(new CreatedWire(start, end, Option.Some(i), id)));
|
||||
return true;
|
||||
}
|
||||
case false when two.IsOutput:
|
||||
{
|
||||
CircuitBoxConnectorIdentifier start = CircuitBoxConnectorIdentifier.FromConnection(two),
|
||||
end = CircuitBoxConnectorIdentifier.FromConnection(one);
|
||||
if (IsExternalConnection(one) || IsExternalConnection(two))
|
||||
{
|
||||
CreateWireWithoutItem(two, one, id, selectedWirePrefab);
|
||||
onCreated(new CreatedWire(start, end, Option.None, id));
|
||||
return true;
|
||||
}
|
||||
|
||||
CreateWireWithItem(two, one, selectedWirePrefab, id, i => onCreated(new CreatedWire(start, end, Option.Some(i), id)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool VerifyConnection(CircuitBoxConnection one, CircuitBoxConnection two)
|
||||
{
|
||||
if (one.IsOutput == two.IsOutput || one == two) { return false; }
|
||||
|
||||
if (one is CircuitBoxNodeConnection oneNodeConnection &&
|
||||
two is CircuitBoxNodeConnection twoNodeConnection)
|
||||
{
|
||||
if (oneNodeConnection.Component == twoNodeConnection.Component)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (one is CircuitBoxNodeConnection { HasAvailableSlots: false } ||
|
||||
two is CircuitBoxNodeConnection { HasAvailableSlots: false })
|
||||
{
|
||||
return one is not CircuitBoxNodeConnection || two is not CircuitBoxNodeConnection;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsExternalConnection(CircuitBoxConnection conn) => conn is (CircuitBoxInputConnection or CircuitBoxOutputConnection);
|
||||
|
||||
private void CreateWireWithoutItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort id, ItemPrefab prefab)
|
||||
{
|
||||
bool hasExternalConnection = false;
|
||||
if (one is CircuitBoxInputConnection input)
|
||||
{
|
||||
hasExternalConnection = true;
|
||||
input.ExternallyConnectedTo.Add(two);
|
||||
}
|
||||
|
||||
if (two is CircuitBoxOutputConnection output)
|
||||
{
|
||||
hasExternalConnection = true;
|
||||
one.Connection.CircuitBoxConnections.Add(output);
|
||||
}
|
||||
|
||||
if (hasExternalConnection)
|
||||
{
|
||||
two.ExternallyConnectedFrom.Add(one);
|
||||
}
|
||||
|
||||
AddWireDirect(id, prefab, Option.None, one, two);
|
||||
}
|
||||
|
||||
private void CreateWireWithItem(CircuitBoxConnection one, CircuitBoxConnection two, ItemPrefab prefab, ushort wireId, Action<Item> onItemSpawned)
|
||||
{
|
||||
if (WireContainer is null) { return; }
|
||||
|
||||
if (IsExternalConnection(one) || IsExternalConnection(two))
|
||||
{
|
||||
DebugConsole.ThrowError("Cannot add a wire between an external connection and a component connection.");
|
||||
return;
|
||||
}
|
||||
|
||||
SpawnItem(this, prefab, WireContainer, wire =>
|
||||
{
|
||||
AddWireDirect(wireId, prefab, Option.Some(wire), one, two);
|
||||
onItemSpawned(wire);
|
||||
});
|
||||
}
|
||||
|
||||
private void CreateWireWithItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort wireId, Item it)
|
||||
{
|
||||
if (IsExternalConnection(one) || IsExternalConnection(two))
|
||||
{
|
||||
DebugConsole.ThrowError("Cannot add a wire between an external connection and a component connection.");
|
||||
return;
|
||||
}
|
||||
|
||||
AddWireDirect(wireId, it.Prefab, Option.Some(it), one, two);
|
||||
}
|
||||
|
||||
private void AddWireDirect(ushort id, ItemPrefab prefab, Option<Item> backingItem, CircuitBoxConnection one, CircuitBoxConnection two)
|
||||
=> Wires.Add(new CircuitBoxWire(this, id, backingItem, one, two, prefab));
|
||||
|
||||
private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Action<Item> onItemSpawned)
|
||||
{
|
||||
if (id is ICircuitBoxIdentifiable.NullComponentID)
|
||||
{
|
||||
DebugConsole.ThrowError("Unable to add component because there are no free IDs.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ComponentContainer?.Inventory is { } inventory && inventory.HowManyCanBePut(prefab) <= 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Unable to add component because there is no space in the inventory.");
|
||||
return false;
|
||||
}
|
||||
|
||||
SpawnItem(this, prefab, ComponentContainer, spawnedItem =>
|
||||
{
|
||||
Components.Add(new CircuitBoxComponent(id, spawnedItem, pos, this, usedResource));
|
||||
onItemSpawned(spawnedItem);
|
||||
});
|
||||
|
||||
OnViewUpdateProjSpecific();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unsafe because it doesn't perform error checking since it's data we get from the server
|
||||
private void AddComponentInternalUnsafe(ushort id, Item backingItem, ItemPrefab usedResource, Vector2 pos)
|
||||
{
|
||||
Components.Add(new CircuitBoxComponent(id, backingItem, pos, this, usedResource));
|
||||
OnViewUpdateProjSpecific();
|
||||
}
|
||||
|
||||
private static void ClearSelectionFor(ushort characterId, IReadOnlyCollection<CircuitBoxSelectable> nodes)
|
||||
{
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.SelectedBy != characterId) { continue; }
|
||||
|
||||
node.SetSelected(Option.None);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearAllSelectionsInternal(ushort characterId)
|
||||
{
|
||||
ClearSelectionFor(characterId, Components);
|
||||
ClearSelectionFor(characterId, InputOutputNodes);
|
||||
ClearSelectionFor(characterId, Wires);
|
||||
}
|
||||
|
||||
private void SelectComponentsInternal(IReadOnlyCollection<ushort> ids, ushort characterId, bool overwrite)
|
||||
{
|
||||
if (overwrite) { ClearSelectionFor(characterId, Components); }
|
||||
|
||||
if (!ids.Any()) { return; }
|
||||
|
||||
foreach (CircuitBoxComponent node in Components)
|
||||
{
|
||||
if (!ids.Contains(node.ID)) { continue; }
|
||||
|
||||
node.SetSelected(Option.Some(characterId));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSelections(ImmutableDictionary<ushort, Option<ushort>> nodeIds,
|
||||
ImmutableDictionary<ushort, Option<ushort>> wireIds,
|
||||
ImmutableDictionary<CircuitBoxInputOutputNode.Type, Option<ushort>> inputOutputs)
|
||||
{
|
||||
foreach (var wire in Wires)
|
||||
{
|
||||
if (!wireIds.TryGetValue(wire.ID, out var selectedBy)) { continue; }
|
||||
|
||||
if (selectedBy.TryUnwrap(out var id))
|
||||
{
|
||||
wire.IsSelected = true;
|
||||
wire.SelectedBy = id;
|
||||
continue;
|
||||
}
|
||||
|
||||
wire.IsSelected = false;
|
||||
wire.SelectedBy = 0;
|
||||
}
|
||||
|
||||
foreach (var node in Components)
|
||||
{
|
||||
if (!nodeIds.TryGetValue(node.ID, out var selectedBy)) { continue; }
|
||||
|
||||
node.SetSelected(selectedBy);
|
||||
}
|
||||
|
||||
foreach (var node in InputOutputNodes)
|
||||
{
|
||||
if (!inputOutputs.TryGetValue(node.NodeType, out var selectedBy)) { continue; }
|
||||
|
||||
node.SetSelected(selectedBy);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectWiresInternal(IReadOnlyCollection<ushort> ids, ushort characterId, bool overwrite)
|
||||
{
|
||||
if (overwrite) { ClearSelectionFor(characterId, Wires); }
|
||||
|
||||
foreach (CircuitBoxWire wire in Wires)
|
||||
{
|
||||
if (!ids.Contains(wire.ID)) { continue; }
|
||||
|
||||
wire.SetSelected(Option.Some(characterId));
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectInputOutputInternal(IReadOnlyCollection<CircuitBoxInputOutputNode.Type> io, ushort characterId, bool overwrite)
|
||||
{
|
||||
if (overwrite) { ClearSelectionFor(characterId, InputOutputNodes); }
|
||||
|
||||
foreach (var node in InputOutputNodes)
|
||||
{
|
||||
if (!io.Contains(node.NodeType)) { continue; }
|
||||
|
||||
node.SetSelected(Option.Some(characterId));
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveComponentInternal(IReadOnlyCollection<ushort> ids)
|
||||
{
|
||||
foreach (CircuitBoxComponent node in Components.ToImmutableArray())
|
||||
{
|
||||
if (!ids.Contains(node.ID)) { continue; }
|
||||
|
||||
Components.Remove(node);
|
||||
node.Remove();
|
||||
|
||||
foreach (CircuitBoxWire wire in Wires.ToImmutableArray())
|
||||
{
|
||||
if (node.Connectors.Contains(wire.From) || node.Connectors.Contains(wire.To))
|
||||
{
|
||||
RemoveWireCollectionUnsafe(wire);
|
||||
}
|
||||
}
|
||||
}
|
||||
OnViewUpdateProjSpecific();
|
||||
}
|
||||
|
||||
private void RemoveWireInternal(IReadOnlyCollection<ushort> ids)
|
||||
{
|
||||
foreach (CircuitBoxWire wire in Wires.ToImmutableArray())
|
||||
{
|
||||
if (!ids.Contains(wire.ID)) { continue; }
|
||||
|
||||
RemoveWireCollectionUnsafe(wire);
|
||||
}
|
||||
|
||||
OnViewUpdateProjSpecific();
|
||||
}
|
||||
|
||||
private void RemoveWireCollectionUnsafe(CircuitBoxWire wire)
|
||||
{
|
||||
foreach (CircuitBoxOutputConnection output in Outputs)
|
||||
{
|
||||
output.Connection.CircuitBoxConnections.Remove(wire.From);
|
||||
}
|
||||
|
||||
wire.From.Connection.CircuitBoxConnections.Remove(wire.To);
|
||||
|
||||
if (wire.From is CircuitBoxInputConnection input)
|
||||
{
|
||||
input.ExternallyConnectedTo.Remove(wire.To);
|
||||
}
|
||||
|
||||
wire.To.ExternallyConnectedFrom.Remove(wire.From);
|
||||
wire.From.ExternallyConnectedFrom.Remove(wire.To);
|
||||
|
||||
wire.Remove();
|
||||
Wires.Remove(wire);
|
||||
}
|
||||
|
||||
private void MoveNodesInternal(IReadOnlyCollection<ushort> ids,
|
||||
IReadOnlyCollection<CircuitBoxInputOutputNode.Type> ios,
|
||||
Vector2 moveAmount)
|
||||
{
|
||||
IEnumerable<CircuitBoxComponent> nodes = Components.Where(node => ids.Contains(node.ID));
|
||||
foreach (CircuitBoxComponent node in nodes)
|
||||
{
|
||||
node.Position += moveAmount;
|
||||
}
|
||||
|
||||
|
||||
foreach (var io in InputOutputNodes)
|
||||
{
|
||||
if (!ios.Contains(io.NodeType)) { continue; }
|
||||
io.Position += moveAmount;
|
||||
}
|
||||
|
||||
OnViewUpdateProjSpecific();
|
||||
}
|
||||
|
||||
public override bool Select(Character character)
|
||||
=> item.GetComponent<Holdable>() is not { Attached: false } && base.Select(character);
|
||||
|
||||
public partial void OnViewUpdateProjSpecific();
|
||||
|
||||
partial void InitProjSpecific(ContentXElement element);
|
||||
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
foreach (var input in Inputs)
|
||||
{
|
||||
if (input.Connection != connection) { continue; }
|
||||
|
||||
input.ReceiveSignal(signal);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsRoundRunning()
|
||||
=> !Submarine.Unloading && GameMain.GameSession is { IsRunning: true };
|
||||
|
||||
public static Option<CircuitBox> FindCircuitBox(ushort itemId, byte componentIndex)
|
||||
{
|
||||
if (!IsRoundRunning() || Entity.FindEntityByID(itemId) is not Item item) { return Option.None; }
|
||||
|
||||
if (componentIndex >= item.Components.Count)
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
ItemComponent targetComponent = item.Components[componentIndex];
|
||||
if (targetComponent is CircuitBox circuitBox)
|
||||
{
|
||||
return Option.Some(circuitBox);
|
||||
}
|
||||
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
private ItemContainer? GetContainerOrNull(int index) => index >= 0 && index < containers.Length ? containers[index] : null;
|
||||
|
||||
public void CreateRefundItemsForUsedResources(IReadOnlyCollection<ushort> ids, Character? character)
|
||||
{
|
||||
if (!IsInGame()) { return; }
|
||||
|
||||
var prefabsToCreate = Components.Where(comp => ids.Contains(comp.ID))
|
||||
.Select(static comp => comp.UsedResource)
|
||||
.ToImmutableArray();
|
||||
|
||||
foreach (ItemPrefab prefab in prefabsToCreate)
|
||||
{
|
||||
if (character?.Inventory is null)
|
||||
{
|
||||
Entity.Spawner?.AddItemToSpawnQueue(prefab, item.Position, item.Submarine);
|
||||
}
|
||||
else
|
||||
{
|
||||
Entity.Spawner?.AddItemToSpawnQueue(prefab, character.Inventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ImmutableArray<Item> GetSortedCircuitBoxSortedItemsFromPlayer(Character? character)
|
||||
=> character?.Inventory?.FindAllItems(predicate: CanItemBeAccessed, recursive: true)
|
||||
.OrderBy(static i => i.Prefab.Identifier == Tags.FPGACircuit)
|
||||
.ToImmutableArray() ?? ImmutableArray<Item>.Empty;
|
||||
|
||||
public static bool CanItemBeAccessed(Item item) =>
|
||||
item.ParentInventory switch
|
||||
{
|
||||
ItemInventory ii => ii.Container.DrawInventory,
|
||||
_ => true
|
||||
};
|
||||
|
||||
public static Option<Item> GetApplicableResourcePlayerHas(ItemPrefab prefab, Character? character)
|
||||
{
|
||||
if (character is null) { return Option.None; }
|
||||
|
||||
return GetApplicableResourcePlayerHas(prefab, GetSortedCircuitBoxSortedItemsFromPlayer(character));
|
||||
}
|
||||
|
||||
public static Option<Item> GetApplicableResourcePlayerHas(ItemPrefab prefab, ImmutableArray<Item> playerItems)
|
||||
{
|
||||
foreach (var invItem in playerItems)
|
||||
{
|
||||
if (invItem.Prefab == prefab || invItem.Prefab.Identifier == Tags.FPGACircuit)
|
||||
{
|
||||
return Option.Some(invItem);
|
||||
}
|
||||
}
|
||||
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
public static void SpawnItem(CircuitBox circuitBox, ItemPrefab prefab, ItemContainer? container, Action<Item> onSpawned)
|
||||
{
|
||||
if (container is null)
|
||||
{
|
||||
throw new Exception("Circuit box has no inventory");
|
||||
}
|
||||
|
||||
if (IsInGame())
|
||||
{
|
||||
Entity.Spawner?.AddItemToSpawnQueue(prefab, container.Inventory, onSpawned: onSpawned);
|
||||
return;
|
||||
}
|
||||
|
||||
Item forceSpawnedItem = new Item(prefab, Vector2.Zero, null);
|
||||
container.Inventory.TryPutItem(forceSpawnedItem, null);
|
||||
onSpawned(forceSpawnedItem);
|
||||
}
|
||||
|
||||
public static void RemoveItem(Item item)
|
||||
{
|
||||
if (IsInGame())
|
||||
{
|
||||
Entity.Spawner?.AddItemToRemoveQueue(item);
|
||||
return;
|
||||
}
|
||||
|
||||
item.Remove();
|
||||
}
|
||||
|
||||
public static bool IsInGame()
|
||||
=> Screen.Selected is not { IsEditor: true };
|
||||
|
||||
public static bool IsCircuitBoxSelected(Character character)
|
||||
=> character.SelectedItem?.GetComponent<CircuitBox>() is not null;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,15 @@ namespace Barotrauma.Items.Components
|
||||
private readonly HashSet<Wire> wires;
|
||||
public IReadOnlyCollection<Wire> Wires => wires;
|
||||
|
||||
/// <summary>
|
||||
/// Circuit box input and output connections that are linked to this connection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We don't want to create a wire between the circuit boxes connection panel and the
|
||||
/// connection panel of the item inside the circuit box so we use this to bridge the gap.
|
||||
/// </remarks>
|
||||
public List<CircuitBoxConnection> CircuitBoxConnections = new();
|
||||
|
||||
private bool enumeratingWires;
|
||||
private readonly HashSet<Wire> removedWires = new HashSet<Wire>();
|
||||
|
||||
@@ -177,6 +186,12 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the the connection is connected to a wire or a circuit box connection
|
||||
/// </summary>
|
||||
public bool IsConnectedToSomething()
|
||||
=> wires.Count > 0 || CircuitBoxConnections.Count > 0;
|
||||
|
||||
public void SetRecipientsDirty()
|
||||
{
|
||||
recipientsDirty = true;
|
||||
@@ -304,25 +319,15 @@ namespace Barotrauma.Items.Components
|
||||
if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; }
|
||||
|
||||
signal.source?.LastSentSignalRecipients.Add(recipient);
|
||||
|
||||
Connection connection = recipient;
|
||||
connection.LastReceivedSignal = signal;
|
||||
#if CLIENT
|
||||
wire.RegisterSignal(signal, source: this);
|
||||
#endif
|
||||
SendSignalIntoConnection(signal, recipient);
|
||||
}
|
||||
|
||||
foreach (ItemComponent ic in recipient.item.Components)
|
||||
{
|
||||
ic.ReceiveSignal(signal, connection);
|
||||
}
|
||||
|
||||
if (recipient.Effects != null && signal.value != "0")
|
||||
{
|
||||
foreach (StatusEffect effect in recipient.Effects)
|
||||
{
|
||||
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
|
||||
}
|
||||
}
|
||||
foreach (CircuitBoxConnection connection in CircuitBoxConnections)
|
||||
{
|
||||
connection.ReceiveSignal(signal);
|
||||
}
|
||||
enumeratingWires = false;
|
||||
foreach (var removedWire in removedWires)
|
||||
@@ -331,7 +336,24 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
removedWires.Clear();
|
||||
}
|
||||
|
||||
|
||||
public static void SendSignalIntoConnection(Signal signal, Connection conn)
|
||||
{
|
||||
conn.LastReceivedSignal = signal;
|
||||
|
||||
foreach (ItemComponent ic in conn.item.Components)
|
||||
{
|
||||
ic.ReceiveSignal(signal, conn);
|
||||
}
|
||||
|
||||
if (conn.Effects == null || signal.value == "0") { return; }
|
||||
|
||||
foreach (StatusEffect effect in conn.Effects)
|
||||
{
|
||||
conn.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearConnections()
|
||||
{
|
||||
if (IsPower && Grid != null)
|
||||
|
||||
+13
-5
@@ -9,7 +9,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable
|
||||
{
|
||||
public List<Connection> Connections;
|
||||
const int MaxConnectionCount = 256;
|
||||
public readonly List<Connection> Connections = new List<Connection>();
|
||||
|
||||
private Character user;
|
||||
|
||||
@@ -67,10 +68,13 @@ namespace Barotrauma.Items.Components
|
||||
public ConnectionPanel(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
Connections = new List<Connection>();
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
if (Connections.Count == MaxConnectionCount)
|
||||
{
|
||||
DebugConsole.ThrowError($"Too many connections in the item {item.Prefab.Identifier} (> {MaxConnectionCount}).");
|
||||
break;
|
||||
}
|
||||
switch (subElement.Name.ToString())
|
||||
{
|
||||
case "input":
|
||||
@@ -179,7 +183,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
UpdateProjSpecific(deltaTime);
|
||||
|
||||
if (user == null || user.SelectedItem != item)
|
||||
if (user == null || (user.SelectedItem != item && user.SelectedSecondaryItem != item))
|
||||
{
|
||||
#if SERVER
|
||||
if (user != null) { item.CreateServerEvent(this); }
|
||||
@@ -208,6 +212,10 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public bool CanRewire()
|
||||
{
|
||||
if (item.Container?.GetComponent<CircuitBox>() != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
//attaching wires to items with a body is not allowed
|
||||
//(signal items remove their bodies when attached to a wall)
|
||||
if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic)
|
||||
@@ -395,7 +403,7 @@ namespace Barotrauma.Items.Components
|
||||
#if CLIENT
|
||||
TriggerRewiringSound();
|
||||
#endif
|
||||
|
||||
msg.WriteByte((byte)Connections.Count);
|
||||
foreach (Connection connection in Connections)
|
||||
{
|
||||
msg.WriteVariableUInt32((uint)connection.Wires.Count);
|
||||
|
||||
@@ -299,9 +299,12 @@ namespace Barotrauma.Items.Components
|
||||
//make sure the clients know about the states of the checkboxes and text fields
|
||||
if (customInterfaceElementList.Any())
|
||||
{
|
||||
if (item.Submarine == null || !item.Submarine.Loading)
|
||||
if (item.FullyInitialized)
|
||||
{
|
||||
item.CreateServerEvent(this);
|
||||
CoroutineManager.Invoke(() =>
|
||||
{
|
||||
if (!item.Removed) { item.CreateServerEvent(this); }
|
||||
}, delay: 0.1f);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
[InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)]
|
||||
[InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)]
|
||||
public string FalseOutput
|
||||
{
|
||||
get { return falseOutput; }
|
||||
|
||||
@@ -198,6 +198,22 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the red component of the light is twice as bright as the blue and green. Can be used by StatusEffects.
|
||||
/// </summary>
|
||||
public bool IsRed => ColorExtensions.IsRedDominant(LightColor);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the green component of the light is twice as bright as the red and blue. Can be used by StatusEffects.
|
||||
/// </summary>
|
||||
public bool IsGreen => ColorExtensions.IsGreenDominant(LightColor);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the blue component of the light is twice as bright as the red and green. Can be used by StatusEffects.
|
||||
/// </summary>
|
||||
public bool IsBlue => ColorExtensions.IsBlueDominant(LightColor);
|
||||
|
||||
|
||||
public float TemporaryFlickerTimer;
|
||||
|
||||
public override void Move(Vector2 amount, bool ignoreContacts = false)
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
private string falseOutput;
|
||||
[InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)]
|
||||
[InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)]
|
||||
public string FalseOutput
|
||||
{
|
||||
get { return falseOutput; }
|
||||
@@ -136,7 +136,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
[Editable(DecimalCount = 3), Serialize(0.1f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)]
|
||||
[InGameEditable(DecimalCount = 3), Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)]
|
||||
public float MinimumVelocity
|
||||
{
|
||||
get;
|
||||
@@ -254,45 +254,52 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
if (Target.HasFlag(TargetType.Human) || Target.HasFlag(TargetType.Pet) || Target.HasFlag(TargetType.Monster))
|
||||
bool triggerFromHumans = Target.HasFlag(TargetType.Human);
|
||||
bool triggerFromPets = Target.HasFlag(TargetType.Pet);
|
||||
bool triggerFromMonsters = Target.HasFlag(TargetType.Monster);
|
||||
bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters;
|
||||
if (!hasTriggers) { return; }
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
foreach (Character c in Character.CharacterList)
|
||||
if (IgnoreDead && c.IsDead) { continue; }
|
||||
|
||||
//ignore characters that have spawned a second or less ago
|
||||
//makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground
|
||||
if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; }
|
||||
if (c.IsHuman)
|
||||
{
|
||||
if (IgnoreDead && c.IsDead) { continue; }
|
||||
|
||||
//ignore characters that have spawned a second or less ago
|
||||
//makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground
|
||||
if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; }
|
||||
|
||||
if (c.IsHuman)
|
||||
{
|
||||
if (!Target.HasFlag(TargetType.Human)) { continue; }
|
||||
}
|
||||
else if (c.IsPet)
|
||||
{
|
||||
if (!Target.HasFlag(TargetType.Pet)) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Target.HasFlag(TargetType.Monster)) { continue; }
|
||||
}
|
||||
|
||||
//do a rough check based on the position of the character's collider first
|
||||
//before the more accurate limb-based check
|
||||
if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY)
|
||||
if (!triggerFromHumans) { continue; }
|
||||
}
|
||||
else if (c.IsPet)
|
||||
{
|
||||
if (!triggerFromPets) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a human or a pet -> monster?
|
||||
if (!triggerFromMonsters) { continue; }
|
||||
if (CharacterParams.CompareGroup(c.Group, CharacterPrefab.HumanGroup))
|
||||
{
|
||||
//characters in the "human" group aren't considered monsters (even if they were something like a friendly mudraptor)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Limb limb in c.AnimController.Limbs)
|
||||
//do a rough check based on the position of the character's collider first
|
||||
//before the more accurate limb-based check
|
||||
if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (Limb limb in c.AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; }
|
||||
if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect))
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; }
|
||||
if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect))
|
||||
{
|
||||
MotionDetected = true;
|
||||
return;
|
||||
}
|
||||
MotionDetected = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -50,6 +50,9 @@ namespace Barotrauma.Items.Components
|
||||
[InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)]
|
||||
public bool UseCaptureGroup { get; set; }
|
||||
|
||||
[InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component output the value of a capture group even if it's empty?", alwaysUseInstanceValues: true)]
|
||||
public bool OutputEmptyCaptureGroup { get; set; }
|
||||
|
||||
[InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)]
|
||||
public string FalseOutput { get; set; }
|
||||
|
||||
@@ -120,6 +123,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
string signalOut;
|
||||
bool allowEmptyStringOutput = false;
|
||||
if (previousResult)
|
||||
{
|
||||
if (UseCaptureGroup)
|
||||
@@ -127,6 +131,7 @@ namespace Barotrauma.Items.Components
|
||||
if (previousGroups != null && previousGroups.TryGetValue(Output, out Group group))
|
||||
{
|
||||
signalOut = group.Value;
|
||||
allowEmptyStringOutput = OutputEmptyCaptureGroup;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -143,13 +148,9 @@ namespace Barotrauma.Items.Components
|
||||
signalOut = FalseOutput;
|
||||
}
|
||||
|
||||
if (ContinuousOutput)
|
||||
if (!string.IsNullOrEmpty(signalOut) || (allowEmptyStringOutput && signalOut == string.Empty)) { item.SendSignal(signalOut, "signal_out"); }
|
||||
if (!ContinuousOutput)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); }
|
||||
nonContinuousOutputSent = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)]
|
||||
[InGameEditable, Serialize(true, IsPropertySaveable.Yes, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)]
|
||||
public bool IsOn
|
||||
{
|
||||
get
|
||||
@@ -139,7 +139,7 @@ namespace Barotrauma.Items.Components
|
||||
isBroken = false;
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
|
||||
if (Voltage > OverloadVoltage && CanBeOverloaded && item.Repairables.Any())
|
||||
{
|
||||
|
||||
@@ -10,11 +10,13 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
public readonly string Text;
|
||||
public readonly Color Color;
|
||||
public readonly bool IsWelcomeMessage;
|
||||
|
||||
public TerminalMessage(string text, Color color)
|
||||
public TerminalMessage(string text, Color color, bool isWelcomeMessage)
|
||||
{
|
||||
Text = text;
|
||||
Color = color;
|
||||
IsWelcomeMessage = isWelcomeMessage;
|
||||
}
|
||||
|
||||
public void Deconstruct(out string text, out Color color)
|
||||
@@ -60,7 +62,7 @@ namespace Barotrauma.Items.Components
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) { return; }
|
||||
ShowOnDisplay(value, addToHistory: true, TextColor);
|
||||
ShowOnDisplay(value, addToHistory: true, TextColor, isWelcomeMessage: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +115,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
partial void ShowOnDisplay(string input, bool addToHistory, Color color);
|
||||
partial void ShowOnDisplay(string input, bool addToHistory, Color color, bool isWelcomeMessage);
|
||||
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
@@ -127,7 +129,7 @@ namespace Barotrauma.Items.Components
|
||||
signal.value = signal.value.Substring(0, MaxMessageLength);
|
||||
}
|
||||
string inputSignal = signal.value.Replace("\\n", "\n");
|
||||
ShowOnDisplay(inputSignal, addToHistory: true, TextColor);
|
||||
ShowOnDisplay(inputSignal, addToHistory: true, TextColor, isWelcomeMessage: false);
|
||||
break;
|
||||
case "set_text_color":
|
||||
if (signal.value != prevColorSignal)
|
||||
@@ -160,7 +162,7 @@ namespace Barotrauma.Items.Components
|
||||
base.OnItemLoaded();
|
||||
if (!DisplayedWelcomeMessage.IsNullOrEmpty() && !WelcomeMessageDisplayed)
|
||||
{
|
||||
ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor);
|
||||
ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor, isWelcomeMessage: true);
|
||||
DisplayedWelcomeMessage = "";
|
||||
//disable welcome message if a game session is running so it doesn't reappear on successive rounds
|
||||
if (GameMain.GameSession != null && !isSubEditor)
|
||||
@@ -175,8 +177,13 @@ namespace Barotrauma.Items.Components
|
||||
var componentElement = base.Save(parentElement);
|
||||
for (int i = 0; i < messageHistory.Count; i++)
|
||||
{
|
||||
componentElement.Add(new XAttribute("msg" + i, messageHistory[i].Text));
|
||||
componentElement.Add(new XAttribute("color" + i, messageHistory[i].Color.ToStringHex()));
|
||||
var msg = messageHistory[i];
|
||||
componentElement.Add(new XAttribute("msg" + i, msg.Text));
|
||||
componentElement.Add(new XAttribute("color" + i, msg.Color.ToStringHex()));
|
||||
if (msg.IsWelcomeMessage)
|
||||
{
|
||||
componentElement.Add(new XAttribute("welcomemessage" + i, true));
|
||||
}
|
||||
}
|
||||
return componentElement;
|
||||
}
|
||||
@@ -189,7 +196,8 @@ namespace Barotrauma.Items.Components
|
||||
string msg = componentElement.GetAttributeString("msg" + i, null);
|
||||
if (msg is null) { break; }
|
||||
Color color = componentElement.GetAttributeColor("color" + i, TextColor);
|
||||
ShowOnDisplay(msg, addToHistory: true, color);
|
||||
bool isWelcomeMessage = componentElement.GetAttributeBool("welcomemessage" + i, false);
|
||||
ShowOnDisplay(msg, addToHistory: true, color, isWelcomeMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,10 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public static int GetWaterPercentage(Hull hull)
|
||||
{
|
||||
return hull.WaterVolume > 1.0f ? MathHelper.Clamp((int)Math.Ceiling(hull.WaterPercentage), 0, 100) : 0;
|
||||
//treat less than one pixel of water as "no water"
|
||||
return hull.WaterVolume / hull.Rect.Width > 1.0f ?
|
||||
MathHelper.Clamp((int)Math.Ceiling(hull.WaterPercentage), 0, 100) :
|
||||
0;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
@@ -88,7 +91,7 @@ namespace Barotrauma.Items.Components
|
||||
//item in water -> we definitely want to send the True output
|
||||
isInWater = true;
|
||||
}
|
||||
else if (item.CurrentHull != null && item.CurrentHull.WaterPercentage > 0.0f && item.CurrentHull.WaterVolume > 1.0f)
|
||||
else if (item.CurrentHull != null && GetWaterPercentage(item.CurrentHull) > 0)
|
||||
{
|
||||
//(center of the) item in not water -> check if the water surface is below the bottom of the item's rect
|
||||
if (item.CurrentHull.Surface > item.Rect.Y - item.Rect.Height)
|
||||
|
||||
@@ -89,6 +89,26 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
private float jamTimer;
|
||||
public float JamTimer
|
||||
{
|
||||
get { return jamTimer; }
|
||||
set
|
||||
{
|
||||
if (value > 0)
|
||||
{
|
||||
#if CLIENT
|
||||
if (jamTimer <= 0)
|
||||
{
|
||||
HintManager.OnRadioJammed(Item);
|
||||
}
|
||||
#endif
|
||||
IsActive = true;
|
||||
}
|
||||
jamTimer = Math.Max(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
public WifiComponent(Item item, ContentXElement element)
|
||||
: base (item, element)
|
||||
{
|
||||
@@ -123,8 +143,12 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanTransmit()
|
||||
public bool CanTransmit(bool ignoreJamming = false)
|
||||
{
|
||||
if (!ignoreJamming)
|
||||
{
|
||||
if (jamTimer > 0) { return false; }
|
||||
}
|
||||
return HasRequiredContainedItems(user: null, addMessage: false);
|
||||
}
|
||||
|
||||
@@ -140,12 +164,13 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (sender == null || sender.channel != channel) { return false; }
|
||||
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; }
|
||||
if (jamTimer > 0) { return false; }
|
||||
|
||||
//if the component is not linked to chat and has nothing connected to the output, sending a signal to it does nothing
|
||||
// = no point in receiving
|
||||
if (!LinkToChat)
|
||||
{
|
||||
if (signalOutConnection == null || signalOutConnection.Wires.Count <= 0)
|
||||
if (signalOutConnection == null || !signalOutConnection.IsConnectedToSomething())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -169,12 +194,16 @@ namespace Barotrauma.Items.Components
|
||||
if (sender == null || sender.channel != channel) { return false; }
|
||||
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; }
|
||||
if (Vector2.DistanceSquared(item.WorldPosition, sender.item.WorldPosition) > sender.range * sender.range) { return false; }
|
||||
if (jamTimer > 0) { return false; }
|
||||
return HasRequiredContainedItems(user: null, addMessage: false);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
chatMsgCooldown -= deltaTime;
|
||||
if (chatMsgCooldown <= 0.0f)
|
||||
JamTimer -= deltaTime;
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
if (chatMsgCooldown <= 0.0f && JamTimer <= 0.0f)
|
||||
{
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,14 @@ namespace Barotrauma.Items.Components
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, "If disabled, the wire will not be dropped when connecting. Used in circuit box to store the wires inside the box.")]
|
||||
public bool DropOnConnect
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Wire(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -224,26 +231,31 @@ namespace Barotrauma.Items.Components
|
||||
connections[connectionIndex] = newConnection;
|
||||
FixNodeEnds();
|
||||
|
||||
if (addNode)
|
||||
if (addNode)
|
||||
{
|
||||
AddNode(newConnection, connectionIndex);
|
||||
}
|
||||
|
||||
SetConnectedDirty();
|
||||
|
||||
if (connections[0] != null && connections[1] != null)
|
||||
if (DropOnConnect)
|
||||
{
|
||||
foreach (ItemComponent ic in item.Components)
|
||||
if (connections[0] != null && connections[1] != null)
|
||||
{
|
||||
if (ic == this) { continue; }
|
||||
ic.Drop(null);
|
||||
foreach (ItemComponent ic in item.Components)
|
||||
{
|
||||
if (ic == this) { continue; }
|
||||
|
||||
ic.Drop(null);
|
||||
}
|
||||
|
||||
item.Container?.RemoveContained(item);
|
||||
if (item.body != null) { item.body.Enabled = false; }
|
||||
|
||||
IsActive = false;
|
||||
|
||||
CleanNodes();
|
||||
}
|
||||
item.Container?.RemoveContained(item);
|
||||
if (item.body != null) { item.body.Enabled = false; }
|
||||
|
||||
IsActive = false;
|
||||
|
||||
CleanNodes();
|
||||
}
|
||||
|
||||
if (item.body != null) { item.Submarine = newConnection.Item.Submarine; }
|
||||
|
||||
@@ -304,6 +304,21 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
private float _maxAngleOffset;
|
||||
[Serialize(10.0f, IsPropertySaveable.No, description: "How much off the turret can be from the target for the AI to shoot. In degrees.")]
|
||||
public float MaxAngleOffset
|
||||
{
|
||||
get => _maxAngleOffset;
|
||||
private set => _maxAngleOffset = MathHelper.Clamp(value, 0f, 180f);
|
||||
}
|
||||
|
||||
[Serialize(1.1f, IsPropertySaveable.No, description: "How much does the AI prefer currently selected targets over new targets closer to the turret.")]
|
||||
public float AICurrentTargetPriorityMultiplier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(-1, IsPropertySaveable.Yes, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")]
|
||||
public int MaxActiveProjectiles
|
||||
{
|
||||
@@ -383,7 +398,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
item.IsShootable = true;
|
||||
item.RequireAimToUse = false;
|
||||
isSlowTurret = item.HasTag("slowturret");
|
||||
isSlowTurret = item.HasTag("slowturret".ToIdentifier());
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
@@ -474,7 +489,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
|
||||
float previousChargeTime = currentChargeTime;
|
||||
|
||||
@@ -594,9 +609,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
UpdateLightComponents();
|
||||
|
||||
if (AutoOperate)
|
||||
if (AutoOperate && ActiveUser == null)
|
||||
{
|
||||
UpdateAutoOperate(deltaTime);
|
||||
UpdateAutoOperate(deltaTime, ignorePower: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,7 +702,7 @@ namespace Barotrauma.Items.Components
|
||||
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
|
||||
if (projectileContainer != null && projectileContainer.Item != item)
|
||||
{
|
||||
projectileContainer?.Item.Use(deltaTime, null);
|
||||
projectileContainer?.Item.Use(deltaTime);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -713,7 +728,7 @@ namespace Barotrauma.Items.Components
|
||||
ItemContainer projectileContainer = containerItem.GetComponent<ItemContainer>();
|
||||
if (projectileContainer != null)
|
||||
{
|
||||
containerItem.Use(deltaTime, null);
|
||||
containerItem.Use(deltaTime);
|
||||
projectiles = GetLoadedProjectiles();
|
||||
if (projectiles.Any()) { return true; }
|
||||
}
|
||||
@@ -749,7 +764,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (e is not Item linkedItem) { continue; }
|
||||
if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; }
|
||||
if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource"))
|
||||
if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag(Tags.TurretAmmoSource))
|
||||
{
|
||||
tinkeringStrength = repairable.TinkeringStrength;
|
||||
}
|
||||
@@ -757,28 +772,29 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (!ignorePower)
|
||||
{
|
||||
List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
|
||||
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)
|
||||
var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f);
|
||||
int batteryCount = batteries.Count();
|
||||
if (batteryCount > 0)
|
||||
{
|
||||
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
|
||||
if (!batteries.Any()) { break; }
|
||||
float takePower = neededPower / batteries.Count;
|
||||
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
|
||||
foreach (PowerContainer battery in batteries)
|
||||
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)
|
||||
{
|
||||
neededPower -= takePower;
|
||||
battery.Charge -= takePower / 3600.0f;
|
||||
float takePower = neededPower / batteryCount;
|
||||
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
|
||||
foreach (PowerContainer battery in batteries)
|
||||
{
|
||||
neededPower -= takePower;
|
||||
battery.Charge -= takePower / 3600.0f;
|
||||
#if SERVER
|
||||
battery.Item.CreateServerEvent(battery);
|
||||
battery.Item.CreateServerEvent(battery);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
launchedProjectile = projectiles.FirstOrDefault();
|
||||
@@ -891,9 +907,21 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f);
|
||||
projectile.SetTransform(
|
||||
ConvertUnits.ToSimUnits(GetRelativeFiringPosition()),
|
||||
-(launchRotation ?? rotation) + spread);
|
||||
|
||||
Vector2 launchPos = ConvertUnits.ToSimUnits(GetRelativeFiringPosition());
|
||||
|
||||
//check if there's some other sub between the turret's origin and the launch pos,
|
||||
//and if so, launch at the intersection of the turret and the sub to prevent the projectile from spawning inside the other sub
|
||||
Body pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), launchPos, null, Physics.CollisionWall, allowInsideFixture: true,
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
return f.Body.UserData is not Submarine sub || sub != item.Submarine;
|
||||
});
|
||||
if (pickedBody != null)
|
||||
{
|
||||
launchPos = Submarine.LastPickedPosition;
|
||||
}
|
||||
projectile.SetTransform(launchPos, -(launchRotation ?? rotation) + spread);
|
||||
projectile.UpdateTransform();
|
||||
projectile.Submarine = projectile.body?.Submarine;
|
||||
|
||||
@@ -959,8 +987,15 @@ namespace Barotrauma.Items.Components
|
||||
private float updateTimer;
|
||||
private bool updatePending;
|
||||
|
||||
public void UpdateAutoOperate(float deltaTime, Identifier friendlyTag = default)
|
||||
private float GetTargetPriorityModifier() => currentChargingState == ChargingState.WindingUp ? 10f : AICurrentTargetPriorityMultiplier;
|
||||
|
||||
public void UpdateAutoOperate(float deltaTime, bool ignorePower, Identifier friendlyTag = default)
|
||||
{
|
||||
if (!ignorePower && !HasPowerToShoot())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsActive = true;
|
||||
|
||||
if (friendlyTag.IsEmpty)
|
||||
@@ -1006,8 +1041,12 @@ namespace Barotrauma.Items.Components
|
||||
if (!IsValidTargetForAutoOperate(character, friendlyTag)) { continue; }
|
||||
float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition);
|
||||
if (dist > closestDist) { continue; }
|
||||
if (!CheckTurretAngle(character.WorldPosition)) { continue; }
|
||||
if (!IsWithinAimingRadius(character.WorldPosition)) { continue; }
|
||||
target = character;
|
||||
if (currentTarget != null && target == currentTarget)
|
||||
{
|
||||
priority *= GetTargetPriorityModifier();
|
||||
}
|
||||
closestDist = dist / priority;
|
||||
}
|
||||
}
|
||||
@@ -1022,8 +1061,12 @@ namespace Barotrauma.Items.Components
|
||||
if (dist > closestDist) { continue; }
|
||||
if (dist > shootDistance * shootDistance) { continue; }
|
||||
if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
|
||||
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
|
||||
if (!IsWithinAimingRadius(targetItem.WorldPosition)) { continue; }
|
||||
target = targetItem;
|
||||
if (currentTarget != null && target == currentTarget)
|
||||
{
|
||||
priority *= GetTargetPriorityModifier();
|
||||
}
|
||||
closestDist = dist / priority;
|
||||
}
|
||||
}
|
||||
@@ -1090,10 +1133,10 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
if (target == null) { return; }
|
||||
|
||||
currentTarget = target;
|
||||
|
||||
float angle = -MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition);
|
||||
targetRotation = MathUtils.WrapAngleTwoPi(angle);
|
||||
|
||||
if (Math.Abs(targetRotation - prevTargetRotation) > 0.1f) { updatePending = true; }
|
||||
|
||||
if (target is Hull targetHull)
|
||||
@@ -1106,10 +1149,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!CheckTurretAngle(angle)) { return; }
|
||||
float enemyAngle = MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition);
|
||||
float turretAngle = -rotation;
|
||||
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; }
|
||||
if (!IsWithinAimingRadius(angle)) { return; }
|
||||
if (!IsPointingTowards(target.WorldPosition)) { return; }
|
||||
}
|
||||
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
|
||||
Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition);
|
||||
@@ -1129,7 +1170,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
if (shoot)
|
||||
{
|
||||
TryLaunch(deltaTime, ignorePower: true);
|
||||
TryLaunch(deltaTime, ignorePower: ignorePower);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1149,12 +1190,12 @@ namespace Barotrauma.Items.Components
|
||||
bool canShoot = HasPowerToShoot();
|
||||
if (!canShoot)
|
||||
{
|
||||
List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
|
||||
float lowestCharge = 0.0f;
|
||||
PowerContainer batteryToLoad = null;
|
||||
foreach (PowerContainer battery in batteries)
|
||||
foreach (PowerContainer battery in GetDirectlyConnectedBatteries())
|
||||
{
|
||||
if (!battery.Item.IsInteractable(character)) { continue; }
|
||||
if (battery.OutputDisabled) { continue; }
|
||||
if (batteryToLoad == null || battery.Charge < lowestCharge)
|
||||
{
|
||||
batteryToLoad = battery;
|
||||
@@ -1328,7 +1369,11 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
// Only check the angle to targets that are close enough to be shot at
|
||||
// We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at.
|
||||
if (!CheckTurretAngle(enemy.WorldPosition)) { continue; }
|
||||
if (!IsWithinAimingRadius(enemy.WorldPosition)) { continue; }
|
||||
}
|
||||
if (currentTarget != null && enemy == currentTarget)
|
||||
{
|
||||
priority *= GetTargetPriorityModifier();
|
||||
}
|
||||
targetPos = enemy.WorldPosition;
|
||||
closestEnemy = enemy;
|
||||
@@ -1344,7 +1389,11 @@ namespace Barotrauma.Items.Components
|
||||
if (dist > closestDistance) { continue; }
|
||||
if (dist > shootDistance * shootDistance) { continue; }
|
||||
if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
|
||||
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
|
||||
if (!IsWithinAimingRadius(targetItem.WorldPosition)) { continue; }
|
||||
if (currentTarget != null && targetItem == currentTarget)
|
||||
{
|
||||
priority *= GetTargetPriorityModifier();
|
||||
}
|
||||
targetPos = targetItem.WorldPosition;
|
||||
closestDistance = dist / priority;
|
||||
// Override the target character so that we can target the item instead.
|
||||
@@ -1378,7 +1427,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.Hidden) { continue; }
|
||||
if (!CheckTurretAngle(limb.WorldPosition)) { continue; }
|
||||
if (!IsWithinAimingRadius(limb.WorldPosition)) { continue; }
|
||||
float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
|
||||
if (dist < closestDist)
|
||||
{
|
||||
@@ -1416,14 +1465,14 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 p1 = edge.Point1 + cell.Translation;
|
||||
Vector2 p2 = edge.Point2 + cell.Translation;
|
||||
Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition);
|
||||
if (!CheckTurretAngle(closestPoint))
|
||||
if (!IsWithinAimingRadius(closestPoint))
|
||||
{
|
||||
// The closest point can't be targeted -> get a point directly in front of the turret
|
||||
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
|
||||
if (MathUtils.GetLineSegmentIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection))
|
||||
{
|
||||
closestPoint = intersection;
|
||||
if (!CheckTurretAngle(closestPoint)) { continue; }
|
||||
if (!IsWithinAimingRadius(closestPoint)) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1510,19 +1559,7 @@ namespace Barotrauma.Items.Components
|
||||
character.CursorPosition -= character.Submarine.Position;
|
||||
}
|
||||
|
||||
float enemyAngle = MathUtils.VectorToAngle(targetPos.Value - item.WorldPosition);
|
||||
float turretAngle = -rotation;
|
||||
|
||||
float maxAngleError = 0.15f;
|
||||
if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f)
|
||||
{
|
||||
//larger margin of error if the weapon needs to be charged (-> the bot can start charging when the turret is still rotating towards the target)
|
||||
maxAngleError *= 2.0f;
|
||||
}
|
||||
|
||||
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > maxAngleError) { return false; }
|
||||
|
||||
if (canShoot)
|
||||
if (IsPointingTowards(targetPos.Value))
|
||||
{
|
||||
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
|
||||
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
|
||||
@@ -1551,6 +1588,19 @@ namespace Barotrauma.Items.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsPointingTowards(Vector2 targetPos)
|
||||
{
|
||||
float enemyAngle = MathUtils.VectorToAngle(targetPos - item.WorldPosition);
|
||||
float turretAngle = -rotation;
|
||||
float maxAngleError = MathHelper.ToRadians(MaxAngleOffset);
|
||||
if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f)
|
||||
{
|
||||
//larger margin of error if the weapon needs to be charged (-> the bot can start charging when the turret is still rotating towards the target)
|
||||
maxAngleError *= 2.0f;
|
||||
}
|
||||
return Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) <= maxAngleError;
|
||||
}
|
||||
|
||||
private bool IsTargetItemCloseEnough(Item target, float sqrDist) => float.IsPositiveInfinity(target.Prefab.AITurretTargetingMaxDistance) || sqrDist < MathUtils.Pow2(target.Prefab.AITurretTargetingMaxDistance);
|
||||
|
||||
/// <summary>
|
||||
@@ -1669,6 +1719,7 @@ namespace Barotrauma.Items.Components
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
|
||||
if (f.UserData is Hull) { return false; }
|
||||
return !item.StaticFixtures.Contains(f);
|
||||
});
|
||||
return pickedBody;
|
||||
@@ -1686,7 +1737,7 @@ namespace Barotrauma.Items.Components
|
||||
return new Vector2(item.WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X, item.WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y);
|
||||
}
|
||||
|
||||
private bool CheckTurretAngle(float angle)
|
||||
private bool IsWithinAimingRadius(float angle)
|
||||
{
|
||||
float midRotation = (minRotation + maxRotation) / 2.0f;
|
||||
while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; }
|
||||
@@ -1694,7 +1745,7 @@ namespace Barotrauma.Items.Components
|
||||
return angle >= minRotation && angle <= maxRotation;
|
||||
}
|
||||
|
||||
public bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition));
|
||||
public bool IsWithinAimingRadius(Vector2 target) => IsWithinAimingRadius(-MathUtils.VectorToAngle(target - item.WorldPosition));
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
@@ -1835,7 +1886,7 @@ namespace Barotrauma.Items.Components
|
||||
break;
|
||||
case "trigger_in":
|
||||
if (signal.value == "0") { return; }
|
||||
item.Use((float)Timing.Step, sender);
|
||||
item.Use((float)Timing.Step, user: sender);
|
||||
user = sender;
|
||||
ActiveUser = sender;
|
||||
resetActiveUserTimer = 1f;
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Xml.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Abilities;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -55,6 +56,11 @@ namespace Barotrauma
|
||||
public bool HideOtherWearables => ObscureOtherWearables == ObscuringMode.Hide;
|
||||
public bool AlphaClipOtherWearables => ObscureOtherWearables == ObscuringMode.AlphaClip;
|
||||
public bool CanBeHiddenByOtherWearables { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tags or identifiers of items that can hide this one.
|
||||
/// </summary>
|
||||
public ImmutableHashSet<Identifier> CanBeHiddenByItem { get; private set; }
|
||||
public List<WearableType> HideWearablesOfType { get; private set; }
|
||||
public bool InheritLimbDepth { get; private set; }
|
||||
/// <summary>
|
||||
@@ -139,11 +145,6 @@ namespace Barotrauma
|
||||
case WearableType.Husk:
|
||||
case WearableType.Herpes:
|
||||
Limb = LimbType.Head;
|
||||
ObscureOtherWearables = ObscuringMode.None;
|
||||
InheritLimbDepth = true;
|
||||
InheritScale = true;
|
||||
InheritOrigin = true;
|
||||
InheritSourceRect = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -177,6 +178,10 @@ namespace Barotrauma
|
||||
|
||||
public void ParsePath(bool parseSpritePath)
|
||||
{
|
||||
#if SERVER
|
||||
// Server doesn't care about texture paths at all
|
||||
return;
|
||||
#endif
|
||||
SpritePath = UnassignedSpritePath.Value;
|
||||
if (_picker?.Info != null)
|
||||
{
|
||||
@@ -196,7 +201,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (parseSpritePath)
|
||||
{
|
||||
Sprite.ParseTexturePath(file: SpritePath);
|
||||
Sprite?.ParseTexturePath(file: SpritePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,22 +227,23 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true);
|
||||
CanBeHiddenByItem = SourceElement.GetAttributeIdentifierArray(nameof(CanBeHiddenByItem), Array.Empty<Identifier>()).ToImmutableHashSet();
|
||||
InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true);
|
||||
var scale = SourceElement.GetAttribute("inheritscale");
|
||||
if (scale != null)
|
||||
{
|
||||
InheritScale = scale.GetAttributeBool(false);
|
||||
InheritScale = scale.GetAttributeBool(Type != WearableType.Item);
|
||||
}
|
||||
else
|
||||
{
|
||||
InheritScale = SourceElement.GetAttributeBool("inherittexturescale", false);
|
||||
InheritScale = SourceElement.GetAttributeBool("inherittexturescale", Type != WearableType.Item);
|
||||
}
|
||||
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);
|
||||
InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", Type != WearableType.Item);
|
||||
InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", Type != WearableType.Item);
|
||||
DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true);
|
||||
Sound = SourceElement.GetAttributeString("sound", "");
|
||||
Scale = SourceElement.GetAttributeFloat("scale", 1.0f);
|
||||
@@ -457,22 +463,20 @@ namespace Barotrauma.Items.Components
|
||||
if (!equipLimb.WearingItems.Contains(wearableSprite))
|
||||
{
|
||||
equipLimb.WearingItems.Add(wearableSprite);
|
||||
equipLimb.WearingItems.Sort((i1, i2) => { return i2.Sprite.Depth.CompareTo(i1.Sprite.Depth); });
|
||||
equipLimb.WearingItems.Sort((i1, i2) =>
|
||||
equipLimb.WearingItems.Sort((wearable, nextWearable) =>
|
||||
{
|
||||
if (i1?.WearableComponent == null && i2?.WearableComponent == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if (i1?.WearableComponent == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (i2?.WearableComponent == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
return i1.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(i2.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes));
|
||||
float depth = wearable?.Sprite?.Depth ?? 0;
|
||||
float nextDepth = nextWearable?.Sprite?.Depth ?? 0;
|
||||
return nextDepth.CompareTo(depth);
|
||||
});
|
||||
equipLimb.WearingItems.Sort((wearable, nextWearable) =>
|
||||
{
|
||||
var wearableComponent = wearable?.WearableComponent;
|
||||
var nextWearableComponent = nextWearable?.WearableComponent;
|
||||
if (wearableComponent == null && nextWearableComponent == null) { return 0; }
|
||||
if (wearableComponent == null) { return -1; }
|
||||
if (nextWearableComponent == null) { return 1; }
|
||||
return wearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(nextWearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes));
|
||||
});
|
||||
}
|
||||
#if CLIENT
|
||||
@@ -553,10 +557,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
foreach (WearableSprite wearableSprite in wearableSprites)
|
||||
{
|
||||
if (wearableSprite != null && wearableSprite.Sprite != null)
|
||||
{
|
||||
wearableSprite.Sprite.Remove();
|
||||
}
|
||||
wearableSprite?.Sprite?.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,18 +8,27 @@ using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class Inventory : IServerSerializable, IClientSerializable
|
||||
partial class Inventory
|
||||
{
|
||||
public const int MaxStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63
|
||||
public const int MaxPossibleStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63
|
||||
|
||||
public const int MaxItemsPerNetworkEvent = 128;
|
||||
|
||||
public class ItemSlot
|
||||
{
|
||||
private readonly List<Item> items = new List<Item>(MaxStackSize);
|
||||
private readonly List<Item> items = new List<Item>(MaxPossibleStackSize);
|
||||
|
||||
public bool HideIfEmpty;
|
||||
|
||||
public IReadOnlyList<Item> Items => items;
|
||||
|
||||
private readonly Inventory inventory;
|
||||
|
||||
public ItemSlot(Inventory inventory)
|
||||
{
|
||||
this.inventory = inventory;
|
||||
}
|
||||
|
||||
public bool CanBePut(Item item, bool ignoreCondition = false)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
@@ -41,7 +50,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
if (items[0].Quality != item.Quality) { return false; }
|
||||
if (items[0].Prefab.Identifier != item.Prefab.Identifier || items.Count + 1 > item.Prefab.MaxStackSize)
|
||||
if (items[0].Prefab.Identifier != item.Prefab.Identifier || items.Count + 1 > item.Prefab.GetMaxStackSize(inventory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -80,7 +89,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
if (items[0].Prefab.Identifier != itemPrefab.Identifier ||
|
||||
items.Count + 1 > itemPrefab.MaxStackSize)
|
||||
items.Count + 1 > itemPrefab.GetMaxStackSize(inventory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -92,7 +101,7 @@ namespace Barotrauma
|
||||
public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null, float? condition = null)
|
||||
{
|
||||
if (itemPrefab == null) { return 0; }
|
||||
maxStackSize ??= itemPrefab.MaxStackSize;
|
||||
maxStackSize ??= itemPrefab.GetMaxStackSize(inventory);
|
||||
if (items.Count > 0)
|
||||
{
|
||||
if (condition.HasValue)
|
||||
@@ -135,9 +144,9 @@ namespace Barotrauma
|
||||
{
|
||||
throw new InvalidOperationException("Tried to stack different types of items.");
|
||||
}
|
||||
else if (items.Count + 1 > item.Prefab.MaxStackSize)
|
||||
else if (items.Count + 1 > item.Prefab.GetMaxStackSize(inventory))
|
||||
{
|
||||
throw new InvalidOperationException("Tried to add an item to a full inventory slot (stack already full).");
|
||||
throw new InvalidOperationException($"Tried to add an item to a full inventory slot (stack already full, x{items.Count} {items.First().Prefab.Identifier}).");
|
||||
}
|
||||
}
|
||||
if (items.Contains(item)) { return; }
|
||||
@@ -229,34 +238,11 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// All items contained in the inventory. Stacked items are returned as individual instances. DO NOT modify the contents of the inventory while enumerating this list.
|
||||
/// </summary>
|
||||
public IEnumerable<Item> AllItems
|
||||
public virtual IEnumerable<Item> AllItems
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
foreach (var item in slots[i].Items)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!");
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
|
||||
bool duplicateFound = false;
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
if (slots[j].Items.Contains(item))
|
||||
{
|
||||
duplicateFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!duplicateFound) { yield return item; }
|
||||
}
|
||||
}
|
||||
return GetAllItems(checkForDuplicates: this is CharacterInventory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +278,7 @@ namespace Barotrauma
|
||||
slots = new ItemSlot[capacity];
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
slots[i] = new ItemSlot();
|
||||
slots[i] = new ItemSlot(this);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
@@ -315,6 +301,60 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
protected IEnumerable<Item> GetAllItems(bool checkForDuplicates)
|
||||
{
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
foreach (var item in slots[i].Items)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!");
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checkForDuplicates)
|
||||
{
|
||||
bool duplicateFound = false;
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
if (slots[j].Items.Contains(item))
|
||||
{
|
||||
duplicateFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!duplicateFound) { yield return item; }
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyItemComponentsOfChange()
|
||||
{
|
||||
#if CLIENT
|
||||
if (Owner is Character character && character == Character.Controlled)
|
||||
{
|
||||
character.SelectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (Owner is not Item it) { return; }
|
||||
|
||||
foreach (var c in it.Components)
|
||||
{
|
||||
c.OnInventoryChanged();
|
||||
}
|
||||
|
||||
it.ParentInventory?.NotifyItemComponentsOfChange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the item contained in this inventory. Does not recursively check items inside items.
|
||||
/// </summary>
|
||||
@@ -367,6 +407,13 @@ namespace Barotrauma
|
||||
return slots[index].Items;
|
||||
}
|
||||
|
||||
public int GetItemStackSlotIndex(Item item, int index)
|
||||
{
|
||||
if (index < 0 || index >= slots.Length) { return -1; }
|
||||
|
||||
return slots[index].Items.IndexOf(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the index of the first slot the item is contained in.
|
||||
/// </summary>
|
||||
@@ -534,7 +581,7 @@ namespace Barotrauma
|
||||
var itemInSlot = slots[i].First();
|
||||
if (itemInSlot.OwnInventory != null &&
|
||||
!itemInSlot.OwnInventory.Contains(item) &&
|
||||
(itemInSlot.GetComponent<ItemContainer>()?.GetMaxStackSize(0) ?? 0) == 1 &&
|
||||
itemInSlot.GetComponent<ItemContainer>()?.GetMaxStackSize(0) == 1 &&
|
||||
itemInSlot.OwnInventory.TrySwapping(0, item, user, createNetworkEvent, swapWholeStack: false))
|
||||
{
|
||||
return true;
|
||||
@@ -575,7 +622,7 @@ namespace Barotrauma
|
||||
|
||||
if (removeItem)
|
||||
{
|
||||
item.Drop(user, setTransform: false);
|
||||
item.Drop(user, setTransform: false, createNetworkEvent: createNetworkEvent && GameMain.NetworkMember is { IsServer: true });
|
||||
item.ParentInventory?.RemoveItem(item);
|
||||
}
|
||||
|
||||
@@ -597,7 +644,7 @@ namespace Barotrauma
|
||||
item.body.BodyType = FarseerPhysics.BodyType.Dynamic;
|
||||
item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false);
|
||||
//update to refresh the interpolated draw rotation and position (update doesn't run on disabled bodies)
|
||||
item.body.Update();
|
||||
item.body.UpdateDrawPosition(interpolate: false);
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
@@ -624,6 +671,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotifyItemComponentsOfChange();
|
||||
}
|
||||
|
||||
public bool IsEmpty()
|
||||
@@ -648,7 +697,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!slots[i].Any()) { return false; }
|
||||
var item = slots[i].FirstOrDefault();
|
||||
if (slots[i].Items.Count < item.Prefab.MaxStackSize) { return false; }
|
||||
if (slots[i].Items.Count < item.Prefab.GetMaxStackSize(this)) { return false; }
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -878,21 +927,35 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void CreateNetworkEvent()
|
||||
public void CreateNetworkEvent()
|
||||
{
|
||||
if (GameMain.NetworkMember == null) { return; }
|
||||
if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; }
|
||||
|
||||
if (Owner is Character character)
|
||||
//split into multiple events because one might not be enough to fit all the items
|
||||
List<Range> slotRanges = new List<Range>();
|
||||
int startIndex = 0;
|
||||
int itemCount = 0;
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(character, new Character.InventoryStateEventData());
|
||||
int count = slots[i].Items.Count;
|
||||
if (itemCount + count > MaxItemsPerNetworkEvent || i == capacity - 1)
|
||||
{
|
||||
slotRanges.Add(new Range(startIndex, i + 1));
|
||||
startIndex = i + 1;
|
||||
itemCount = 0;
|
||||
}
|
||||
itemCount += count;
|
||||
}
|
||||
else if (Owner is Item item)
|
||||
|
||||
foreach (var slotRange in slotRanges)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(item, new Item.InventoryStateEventData());
|
||||
CreateNetworkEvent(slotRange);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void CreateNetworkEvent(Range slotRange) { }
|
||||
|
||||
public Item FindItem(Func<Item, bool> predicate, bool recursive)
|
||||
{
|
||||
Item match = AllItems.FirstOrDefault(predicate);
|
||||
@@ -955,9 +1018,12 @@ namespace Barotrauma
|
||||
visualSlots[n].ShowBorderHighlight(Color.White, 0.1f, 0.4f);
|
||||
if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; }
|
||||
}
|
||||
syncItemsDelay = 1.0f;
|
||||
#endif
|
||||
CharacterHUD.RecreateHudTextsIfFocused(item);
|
||||
}
|
||||
|
||||
NotifyItemComponentsOfChange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -989,28 +1055,48 @@ namespace Barotrauma
|
||||
return slots[index].Contains(item);
|
||||
}
|
||||
|
||||
public void SharedRead(IReadMessage msg, out List<ushort>[] newItemIds)
|
||||
public void SharedRead(IReadMessage msg, List<ushort>[] receivedItemIds, out bool readyToApply)
|
||||
{
|
||||
byte slotCount = msg.ReadByte();
|
||||
newItemIds = new List<ushort>[slotCount];
|
||||
for (int i = 0; i < slotCount; i++)
|
||||
byte start = msg.ReadByte();
|
||||
byte end = msg.ReadByte();
|
||||
|
||||
//if we received the first chunk of item IDs, clear the rest
|
||||
//to ensure we don't have anything outdated in the list - we're about to receive the rest of the IDs next
|
||||
if (start == 0)
|
||||
{
|
||||
newItemIds[i] = new List<ushort>();
|
||||
int itemCount = msg.ReadRangedInteger(0, MaxStackSize);
|
||||
for (int j = 0; j < itemCount; j++)
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
newItemIds[i].Add(msg.ReadUInt16());
|
||||
receivedItemIds[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
var newItemIds = new List<ushort>();
|
||||
int itemCount = msg.ReadRangedInteger(0, MaxPossibleStackSize);
|
||||
for (int j = 0; j < itemCount; j++)
|
||||
{
|
||||
newItemIds.Add(msg.ReadUInt16());
|
||||
}
|
||||
receivedItemIds[i] = newItemIds;
|
||||
}
|
||||
|
||||
//if all IDs haven't been received yet (chunked into multiple events?)
|
||||
//don't apply the state yet
|
||||
readyToApply = !receivedItemIds.Contains(null);
|
||||
}
|
||||
|
||||
public void SharedWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
|
||||
public void SharedWrite(IWriteMessage msg, Range slotRange)
|
||||
{
|
||||
msg.WriteByte((byte)capacity);
|
||||
for (int i = 0; i < capacity; i++)
|
||||
int start = slotRange.Start.Value;
|
||||
int end = slotRange.End.Value;
|
||||
|
||||
msg.WriteByte((byte)start);
|
||||
msg.WriteByte((byte)end);
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxStackSize);
|
||||
for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxStackSize); j++)
|
||||
msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxPossibleStackSize);
|
||||
for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxPossibleStackSize); j++)
|
||||
{
|
||||
var item = slots[i].Items[j];
|
||||
msg.WriteUInt16(item?.ID ?? (ushort)0);
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Barotrauma
|
||||
{
|
||||
partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable
|
||||
{
|
||||
public static List<Item> ItemList = new List<Item>();
|
||||
public static readonly List<Item> ItemList = new List<Item>();
|
||||
|
||||
private static readonly HashSet<Item> dangerousItems = new HashSet<Item>();
|
||||
|
||||
@@ -77,17 +77,17 @@ namespace Barotrauma
|
||||
public CampaignMode.InteractionType CampaignInteractionType
|
||||
{
|
||||
get { return campaignInteractionType; }
|
||||
set
|
||||
{
|
||||
if (campaignInteractionType != value)
|
||||
{
|
||||
campaignInteractionType = value;
|
||||
AssignCampaignInteractionTypeProjSpecific(campaignInteractionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType);
|
||||
public void AssignCampaignInteractionType(CampaignMode.InteractionType interactionType, IEnumerable<Client> targetClients = null)
|
||||
{
|
||||
if (campaignInteractionType == interactionType) { return; }
|
||||
campaignInteractionType = interactionType;
|
||||
AssignCampaignInteractionTypeProjSpecific(campaignInteractionType, targetClients);
|
||||
}
|
||||
|
||||
|
||||
partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable<Client> targetClients);
|
||||
|
||||
public bool Visible = true;
|
||||
|
||||
@@ -105,6 +105,12 @@ namespace Barotrauma
|
||||
private readonly List<IDrawableComponent> drawableComponents;
|
||||
private bool hasComponentsToDraw;
|
||||
|
||||
/// <summary>
|
||||
/// Has everything in the item been loaded/instantiated/initialized (basically, can be used to check if the whole constructor/Load method has run).
|
||||
/// Most commonly used to avoid creating network events when some value changes if the item is being initialized.
|
||||
/// </summary>
|
||||
public bool FullyInitialized { get; private set; }
|
||||
|
||||
public PhysicsBody body;
|
||||
private readonly float originalWaterDragCoefficient;
|
||||
private float? overrideWaterDragCoefficient;
|
||||
@@ -114,6 +120,21 @@ namespace Barotrauma
|
||||
set => overrideWaterDragCoefficient = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used by StatusEffects to set the type of the body (if the item has one)
|
||||
/// </summary>
|
||||
public BodyType BodyType
|
||||
{
|
||||
get { return body?.BodyType ?? BodyType.Dynamic; }
|
||||
set
|
||||
{
|
||||
if (body != null)
|
||||
{
|
||||
body.BodyType = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the override value -> falls back to using the original value defined in the xml.
|
||||
/// </summary>
|
||||
@@ -125,9 +146,10 @@ namespace Barotrauma
|
||||
|
||||
private bool transformDirty = true;
|
||||
|
||||
private static readonly List<Item> itemsWithPendingConditionUpdates = new List<Item>();
|
||||
|
||||
private float lastSentCondition;
|
||||
private float sendConditionUpdateTimer;
|
||||
private bool conditionUpdatePending;
|
||||
|
||||
private float prevCondition;
|
||||
private float condition;
|
||||
@@ -207,7 +229,11 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
parentInventory = value;
|
||||
if (parentInventory != null) { Container = parentInventory.Owner as Item; }
|
||||
if (parentInventory != null)
|
||||
{
|
||||
Container = parentInventory.Owner as Item;
|
||||
RemoveFromDroppedStack(allowClientExecute: false);
|
||||
}
|
||||
#if SERVER
|
||||
PreviousParentInventory = value;
|
||||
#endif
|
||||
@@ -229,7 +255,6 @@ namespace Barotrauma
|
||||
container = value;
|
||||
CheckCleanable();
|
||||
SetActiveSprite();
|
||||
|
||||
RefreshRootContainer();
|
||||
}
|
||||
}
|
||||
@@ -247,6 +272,37 @@ namespace Barotrauma
|
||||
set { description = value; }
|
||||
}
|
||||
|
||||
private string descriptionTag;
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork)]
|
||||
/// <summary>
|
||||
/// Can be used to set a localized description via StatusEffects
|
||||
/// </summary>
|
||||
public string DescriptionTag
|
||||
{
|
||||
get { return descriptionTag; }
|
||||
set
|
||||
{
|
||||
if (value == descriptionTag) { return; }
|
||||
if (value.IsNullOrEmpty())
|
||||
{
|
||||
descriptionTag = null;
|
||||
description = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
description = TextManager.Get(value).Value;
|
||||
descriptionTag = value;
|
||||
}
|
||||
if (FullyInitialized &&
|
||||
SerializableProperties != null &&
|
||||
SerializableProperties.TryGetValue(nameof(DescriptionTag).ToIdentifier(), out SerializableProperty property))
|
||||
{
|
||||
GameMain.NetworkMember?.CreateEntityEvent(this, new ChangePropertyEventData(property, this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Editable, Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public bool NonInteractable
|
||||
{
|
||||
@@ -675,6 +731,8 @@ namespace Barotrauma
|
||||
set { allowStealing = value; }
|
||||
}
|
||||
|
||||
public bool IsSalvageMissionItem;
|
||||
|
||||
private string originalOutpost;
|
||||
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public string OriginalOutpost
|
||||
@@ -875,8 +933,8 @@ namespace Barotrauma
|
||||
{
|
||||
get { return allPropertyObjects; }
|
||||
}
|
||||
|
||||
public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam;
|
||||
|
||||
public bool IgnoreByAI(Character character) => HasTag(Barotrauma.Tags.ItemIgnoredByAI) || OrderedToBeIgnored && character.IsOnPlayerTeam;
|
||||
public bool OrderedToBeIgnored { get; set; }
|
||||
|
||||
public bool HasBallastFloraInHull
|
||||
@@ -896,6 +954,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool InPlayerSubmarine => Submarine?.Info is { IsPlayer: true };
|
||||
public bool InBeaconStation => Submarine?.Info is { Type: SubmarineType.BeaconStation };
|
||||
|
||||
public bool IsLadder { get; }
|
||||
|
||||
public bool IsSecondaryItem { get; }
|
||||
@@ -910,6 +971,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timing.TotalTimeUnpaused when some character was last eating the item
|
||||
/// </summary>
|
||||
public float LastEatenTime { get; set; }
|
||||
|
||||
public Action<Character> OnDeselect;
|
||||
|
||||
public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true)
|
||||
@@ -1157,10 +1223,15 @@ namespace Barotrauma
|
||||
DebugConsole.Log("Created " + Name + " (" + ID + ")");
|
||||
|
||||
if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; }
|
||||
if (HasTag("logic")) { isLogic = true; }
|
||||
if (HasTag(Barotrauma.Tags.LogicItem)) { isLogic = true; }
|
||||
|
||||
ApplyStatusEffects(ActionType.OnSpawn, 1.0f);
|
||||
RecalculateConditionValues();
|
||||
|
||||
if (callOnItemLoaded)
|
||||
{
|
||||
FullyInitialized = true;
|
||||
}
|
||||
#if CLIENT
|
||||
Submarine.ForceVisibilityRecheck();
|
||||
#endif
|
||||
@@ -1179,7 +1250,7 @@ namespace Barotrauma
|
||||
};
|
||||
foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
|
||||
{
|
||||
if (!property.Value.Attributes.OfType<Editable>().Any()) continue;
|
||||
if (property.Value.Attributes.OfType<Serialize>().None()) { continue; }
|
||||
clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
|
||||
}
|
||||
|
||||
@@ -1194,9 +1265,10 @@ namespace Barotrauma
|
||||
|
||||
for (int i = 0; i < components.Count && i < clone.components.Count; i++)
|
||||
{
|
||||
foreach (KeyValuePair<Identifier, SerializableProperty> property in components[i].SerializableProperties)
|
||||
//order the properties to get them to be applied in a consistent order (may matter for properties that are interconnected somehow, like IsOn/IsActive)
|
||||
foreach (KeyValuePair<Identifier, SerializableProperty> property in components[i].SerializableProperties.OrderBy(s => s.Key))
|
||||
{
|
||||
if (!property.Value.Attributes.OfType<Editable>().Any()) continue;
|
||||
if (property.Value.Attributes.OfType<Serialize>().None()) { continue; }
|
||||
clone.components[i].SerializableProperties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i]));
|
||||
}
|
||||
|
||||
@@ -1219,18 +1291,48 @@ namespace Barotrauma
|
||||
|
||||
if (FlippedX) clone.FlipX(false);
|
||||
if (FlippedY) clone.FlipY(false);
|
||||
|
||||
|
||||
foreach (ItemComponent component in clone.components)
|
||||
{
|
||||
component.OnItemLoaded();
|
||||
}
|
||||
|
||||
foreach (Item containedItem in ContainedItems)
|
||||
Dictionary<ushort, Item> clonedContainedItems = new();
|
||||
|
||||
for (int i = 0; i < components.Count && i < clone.components.Count; i++)
|
||||
{
|
||||
var containedClone = containedItem.Clone();
|
||||
clone.ownInventory.TryPutItem(containedClone as Item, null);
|
||||
ItemComponent component = components[i],
|
||||
cloneComp = clone.components[i];
|
||||
|
||||
if (component is not ItemContainer origInv ||
|
||||
cloneComp is not ItemContainer cloneInv)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var containedItem in origInv.Inventory.AllItems)
|
||||
{
|
||||
var containedClone = (Item)containedItem.Clone();
|
||||
|
||||
cloneInv.Inventory.TryPutItem(containedClone, null);
|
||||
clonedContainedItems.Add(containedItem.ID, containedClone);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (int i = 0; i < components.Count && i < clone.components.Count; i++)
|
||||
{
|
||||
ItemComponent component = components[i],
|
||||
cloneComp = clone.components[i];
|
||||
|
||||
if (component is not CircuitBox origBox || cloneComp is not CircuitBox cloneBox)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cloneBox.CloneFrom(origBox, clonedContainedItems);
|
||||
}
|
||||
|
||||
clone.FullyInitialized = true;
|
||||
return clone;
|
||||
}
|
||||
|
||||
@@ -1432,7 +1534,7 @@ namespace Barotrauma
|
||||
var pickable = GetComponent<Pickable>();
|
||||
if (pickable != null && !pickable.IsAttached &&
|
||||
Prefab.PreferredContainers.Any() &&
|
||||
(container == null || container.HasTag("allowcleanup")))
|
||||
(container == null || container.HasTag(Barotrauma.Tags.AllowCleanup)))
|
||||
{
|
||||
if (!cleanableItems.Contains(this))
|
||||
{
|
||||
@@ -1638,12 +1740,6 @@ namespace Barotrauma
|
||||
tags.Add(tag);
|
||||
}
|
||||
|
||||
|
||||
public bool HasTag(string tag)
|
||||
{
|
||||
return HasTag(tag.ToIdentifier());
|
||||
}
|
||||
|
||||
public bool HasTag(Identifier tag)
|
||||
{
|
||||
if (tag == null) { return true; }
|
||||
@@ -1690,7 +1786,7 @@ namespace Barotrauma
|
||||
|
||||
public bool ConditionalMatches(PropertyConditional conditional)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conditional.TargetItemComponentName))
|
||||
if (string.IsNullOrEmpty(conditional.TargetItemComponent))
|
||||
{
|
||||
if (!conditional.Matches(this)) { return false; }
|
||||
}
|
||||
@@ -1698,7 +1794,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (ItemComponent component in components)
|
||||
{
|
||||
if (component.Name != conditional.TargetItemComponentName) { continue; }
|
||||
if (component.Name != conditional.TargetItemComponent) { continue; }
|
||||
if (!conditional.Matches(component)) { return false; }
|
||||
}
|
||||
}
|
||||
@@ -1796,7 +1892,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
||||
{
|
||||
targets.AddRange(character.AnimController.Limbs.ToList());
|
||||
targets.AddRange(character.AnimController.Limbs);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb == null && effect.targetLimbs != null)
|
||||
{
|
||||
@@ -1811,7 +1907,7 @@ namespace Barotrauma
|
||||
targets.Add(limb);
|
||||
}
|
||||
|
||||
if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.Add(Container); }
|
||||
if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.AddRange(Container.AllPropertyObjects); }
|
||||
|
||||
effect.Apply(type, deltaTime, this, targets, worldPosition);
|
||||
}
|
||||
@@ -1897,21 +1993,20 @@ namespace Barotrauma
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
if (Math.Abs(lastSentCondition - condition) > 1.0f)
|
||||
{
|
||||
conditionUpdatePending = true;
|
||||
isActive = true;
|
||||
}
|
||||
else if (wasInFullCondition != IsFullCondition)
|
||||
{
|
||||
conditionUpdatePending = true;
|
||||
isActive = true;
|
||||
}
|
||||
else if (!MathUtils.NearlyEqual(lastSentCondition, condition) && (condition <= 0.0f || condition >= MaxCondition))
|
||||
bool needsConditionUpdate = false;
|
||||
if (!MathUtils.NearlyEqual(lastSentCondition, condition) && (condition <= 0.0f || condition >= MaxCondition))
|
||||
{
|
||||
//send the update immediately if the condition changed to max or min
|
||||
sendConditionUpdateTimer = 0.0f;
|
||||
conditionUpdatePending = true;
|
||||
isActive = true;
|
||||
needsConditionUpdate = true;
|
||||
}
|
||||
else if (Math.Abs(lastSentCondition - condition) > 1.0f || wasInFullCondition != IsFullCondition)
|
||||
{
|
||||
needsConditionUpdate = true;
|
||||
}
|
||||
if (needsConditionUpdate && !itemsWithPendingConditionUpdates.Contains(this))
|
||||
{
|
||||
itemsWithPendingConditionUpdates.Add(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1967,12 +2062,16 @@ namespace Barotrauma
|
||||
public void SendPendingNetworkUpdates()
|
||||
{
|
||||
if (!(GameMain.NetworkMember is { IsServer: true })) { return; }
|
||||
if (!conditionUpdatePending) { return; }
|
||||
if (!itemsWithPendingConditionUpdates.Contains(this)) { return; }
|
||||
SendPendingNetworkUpdatesInternal();
|
||||
itemsWithPendingConditionUpdates.Remove(this);
|
||||
}
|
||||
|
||||
private void SendPendingNetworkUpdatesInternal()
|
||||
{
|
||||
CreateStatusEvent(loadingRound: false);
|
||||
lastSentCondition = condition;
|
||||
sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
|
||||
conditionUpdatePending = false;
|
||||
}
|
||||
|
||||
public void CreateStatusEvent(bool loadingRound)
|
||||
@@ -1980,21 +2079,32 @@ namespace Barotrauma
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound));
|
||||
}
|
||||
|
||||
public static void UpdatePendingConditionUpdates(float deltaTime)
|
||||
{
|
||||
if (GameMain.NetworkMember is not { IsServer: true }) { return; }
|
||||
for (int i = 0; i < itemsWithPendingConditionUpdates.Count; i++)
|
||||
{
|
||||
var item = itemsWithPendingConditionUpdates[i];
|
||||
if (item == null || item.Removed)
|
||||
{
|
||||
itemsWithPendingConditionUpdates.RemoveAt(i--);
|
||||
continue;
|
||||
}
|
||||
if (item.Submarine is { Loading: true }) { continue; }
|
||||
|
||||
item.sendConditionUpdateTimer -= deltaTime;
|
||||
if (item.sendConditionUpdateTimer <= 0.0f)
|
||||
{
|
||||
item.SendPendingNetworkUpdatesInternal();
|
||||
itemsWithPendingConditionUpdates.RemoveAt(i--);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool isActive = true;
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
#if SERVER
|
||||
if (!(Submarine is { Loading: true }))
|
||||
{
|
||||
sendConditionUpdateTimer -= deltaTime;
|
||||
if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f)
|
||||
{
|
||||
SendPendingNetworkUpdates();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!isActive) { return; }
|
||||
|
||||
if (impactQueue != null)
|
||||
@@ -2004,14 +2114,27 @@ namespace Barotrauma
|
||||
HandleCollision(impact);
|
||||
}
|
||||
}
|
||||
if (isDroppedStackOwner && body != null)
|
||||
{
|
||||
foreach (var item in droppedStack)
|
||||
{
|
||||
if (item != this)
|
||||
{
|
||||
item.body.Enabled = false;
|
||||
item.body.SetTransformIgnoreContacts(this.SimPosition, body.Rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (aiTarget != null && aiTarget.NeedsUpdate)
|
||||
{
|
||||
aiTarget.Update(deltaTime);
|
||||
}
|
||||
|
||||
var containedEffectType = parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained;
|
||||
|
||||
ApplyStatusEffects(ActionType.Always, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
|
||||
ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
|
||||
ApplyStatusEffects(containedEffectType, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
|
||||
|
||||
for (int i = 0; i < updateableComponents.Count; i++)
|
||||
{
|
||||
@@ -2019,7 +2142,7 @@ namespace Barotrauma
|
||||
|
||||
if (ic.IsActiveConditionals != null)
|
||||
{
|
||||
if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And)
|
||||
if (ic.IsActiveConditionalComparison == PropertyConditional.LogicalOperatorType.And)
|
||||
{
|
||||
bool shouldBeActive = true;
|
||||
foreach (var conditional in ic.IsActiveConditionals)
|
||||
@@ -2140,6 +2263,7 @@ namespace Barotrauma
|
||||
updateableComponents.Count == 0 &&
|
||||
(aiTarget == null || !aiTarget.NeedsUpdate) &&
|
||||
!hasStatusEffectsOfType[(int)ActionType.Always] &&
|
||||
!hasStatusEffectsOfType[(int)containedEffectType] &&
|
||||
(body == null || !body.Enabled))
|
||||
{
|
||||
#if CLIENT
|
||||
@@ -2651,9 +2775,20 @@ namespace Barotrauma
|
||||
|
||||
public bool TryInteract(Character user, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceUseKey = false)
|
||||
{
|
||||
if (CampaignMode.BlocksInteraction(CampaignInteractionType))
|
||||
var campaignInteractionType = CampaignInteractionType;
|
||||
#if SERVER
|
||||
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == user);
|
||||
if (ownerClient != null)
|
||||
{
|
||||
return false;
|
||||
if (!campaignInteractionTypePerClient.TryGetValue(ownerClient, out campaignInteractionType))
|
||||
{
|
||||
campaignInteractionType = CampaignMode.InteractionType.None;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (CampaignMode.BlocksInteraction(campaignInteractionType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool picked = false, selected = false;
|
||||
@@ -2821,9 +2956,9 @@ namespace Barotrauma
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Use(float deltaTime, Character character = null, Limb targetLimb = null)
|
||||
public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null)
|
||||
{
|
||||
if (RequireAimToUse && (character == null || !character.IsKeyDown(InputType.Aim)))
|
||||
if (RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -2836,17 +2971,17 @@ namespace Barotrauma
|
||||
{
|
||||
bool isControlled = false;
|
||||
#if CLIENT
|
||||
isControlled = character == Character.Controlled;
|
||||
isControlled = user == Character.Controlled;
|
||||
#endif
|
||||
if (!ic.HasRequiredContainedItems(character, isControlled)) { continue; }
|
||||
if (ic.Use(deltaTime, character))
|
||||
if (!ic.HasRequiredContainedItems(user, isControlled)) { continue; }
|
||||
if (ic.Use(deltaTime, user))
|
||||
{
|
||||
ic.WasUsed = true;
|
||||
#if CLIENT
|
||||
ic.PlaySound(ActionType.OnUse, character);
|
||||
ic.PlaySound(ActionType.OnUse, user);
|
||||
#endif
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: character, user: character);
|
||||
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, user, targetLimb, useTarget: useTarget, user: user);
|
||||
ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user));
|
||||
if (ic.DeleteOnUse) { remove = true; }
|
||||
}
|
||||
}
|
||||
@@ -2876,7 +3011,7 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
ic.PlaySound(ActionType.OnSecondaryUse, character);
|
||||
#endif
|
||||
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character);
|
||||
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character, useTarget: character);
|
||||
|
||||
if (ic.DeleteOnUse) { remove = true; }
|
||||
}
|
||||
@@ -3009,10 +3144,10 @@ namespace Barotrauma
|
||||
Container = null;
|
||||
}
|
||||
|
||||
if (parentInventory != null)
|
||||
if (ParentInventory != null)
|
||||
{
|
||||
parentInventory.RemoveItem(this);
|
||||
parentInventory = null;
|
||||
ParentInventory.RemoveItem(this);
|
||||
ParentInventory = null;
|
||||
}
|
||||
|
||||
SetContainedItemPositions();
|
||||
@@ -3021,6 +3156,108 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
private List<Item> droppedStack;
|
||||
public IEnumerable<Item> DroppedStack => droppedStack ?? Enumerable.Empty<Item>();
|
||||
|
||||
private bool isDroppedStackOwner;
|
||||
|
||||
/// <summary>
|
||||
/// "Merges" the set of items so they behave as one physical object and can be picked up by clicking once.
|
||||
/// The items need to be instances of the same prefab and have a physics body.
|
||||
/// </summary>
|
||||
public void CreateDroppedStack(IEnumerable<Item> items, bool allowClientExecute)
|
||||
{
|
||||
if (!allowClientExecute && GameMain.NetworkMember is { IsClient: true }) { return; }
|
||||
|
||||
int itemCount = items.Count();
|
||||
|
||||
if (itemCount == 1) { return; }
|
||||
|
||||
if (items.DistinctBy(it => it.Prefab).Count() > 1)
|
||||
{
|
||||
DebugConsole.ThrowError($"Attempted to create a dropped stack of multiple different items ({string.Join(", ", items.DistinctBy(it => it.Prefab))})\n{Environment.StackTrace}");
|
||||
return;
|
||||
}
|
||||
if (items.Any(it => it.body == null))
|
||||
{
|
||||
DebugConsole.ThrowError($"Attempted to create a dropped stack for an item with no body ({items.First().Prefab.Identifier})\n{Environment.StackTrace}");
|
||||
return;
|
||||
}
|
||||
if (items.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"Attempted to create a dropped stack of an empty list of items.\n{Environment.StackTrace}");
|
||||
return;
|
||||
}
|
||||
|
||||
int maxStackSize = items.First().Prefab.MaxStackSize;
|
||||
if (itemCount > maxStackSize)
|
||||
{
|
||||
for (int i = 0; i < MathF.Ceiling(itemCount / maxStackSize); i++)
|
||||
{
|
||||
int startIndex = i * maxStackSize;
|
||||
items.ElementAt(startIndex).CreateDroppedStack(items.Skip(startIndex).Take(maxStackSize), allowClientExecute);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
droppedStack ??= new List<Item>();
|
||||
foreach (Item item in items)
|
||||
{
|
||||
if (!droppedStack.Contains(item))
|
||||
{
|
||||
droppedStack.Add(item);
|
||||
}
|
||||
}
|
||||
SetDroppedStackItemStates();
|
||||
#if SERVER
|
||||
if (GameMain.NetworkMember is { IsServer: true } server)
|
||||
{
|
||||
server.CreateEntityEvent(this, new DroppedStackEventData(droppedStack));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFromDroppedStack(bool allowClientExecute)
|
||||
{
|
||||
if (!allowClientExecute && GameMain.NetworkMember is { IsClient: true }) { return; }
|
||||
if (droppedStack == null) { return; }
|
||||
|
||||
body.Enabled = ParentInventory == null;
|
||||
isDroppedStackOwner = false;
|
||||
droppedStack.Remove(this);
|
||||
SetDroppedStackItemStates();
|
||||
droppedStack = null;
|
||||
#if SERVER
|
||||
if (GameMain.NetworkMember is { IsServer: true } server)
|
||||
{
|
||||
server.CreateEntityEvent(this, new DroppedStackEventData(Enumerable.Empty<Item>()));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void SetDroppedStackItemStates()
|
||||
{
|
||||
if (droppedStack == null) { return; }
|
||||
bool isFirst = true;
|
||||
foreach (Item item in droppedStack)
|
||||
{
|
||||
item.droppedStack = droppedStack;
|
||||
item.isDroppedStackOwner = isFirst;
|
||||
if (item.body != null)
|
||||
{
|
||||
item.body.Enabled = item.body.PhysEnabled = isFirst;
|
||||
if (isFirst)
|
||||
{
|
||||
item.isActive = true;
|
||||
item.body.ResetDynamics();
|
||||
}
|
||||
}
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Equip(Character character)
|
||||
{
|
||||
if (Removed)
|
||||
@@ -3072,7 +3309,7 @@ namespace Barotrauma
|
||||
if (allProperties.Count > 1)
|
||||
{
|
||||
int propertyIndex = allProperties.FindIndex(p => p.property == property && p.obj == entity);
|
||||
if (propertyIndex < -1)
|
||||
if (propertyIndex < 0)
|
||||
{
|
||||
throw new Exception($"Could not find the property \"{property.Name}\" in \"{entity.Name ?? "null"}\"");
|
||||
}
|
||||
@@ -3187,6 +3424,11 @@ namespace Barotrauma
|
||||
propertyIndex = (int)msg.ReadVariableUInt32();
|
||||
}
|
||||
|
||||
if (propertyIndex >= allProperties.Count || propertyIndex < 0)
|
||||
{
|
||||
throw new Exception($"Error in ReadPropertyChange. Property index out of bounds (index: {propertyIndex}, property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})");
|
||||
}
|
||||
|
||||
bool allowEditing = true;
|
||||
object parentObject = allProperties[propertyIndex].obj;
|
||||
SerializableProperty property = allProperties[propertyIndex].property;
|
||||
@@ -3598,7 +3840,9 @@ namespace Barotrauma
|
||||
if (component.Parent != null) { component.IsActive = component.Parent.IsActive; }
|
||||
component.OnItemLoaded();
|
||||
}
|
||||
|
||||
|
||||
item.FullyInitialized = true;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -3811,6 +4055,7 @@ namespace Barotrauma
|
||||
repairableItems.Remove(this);
|
||||
sonarVisibleItems.Remove(this);
|
||||
cleanableItems.Remove(this);
|
||||
RemoveFromDroppedStack(allowClientExecute: true);
|
||||
}
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
@@ -20,9 +22,10 @@ namespace Barotrauma
|
||||
ApplyStatusEffect = 7,
|
||||
Upgrade = 8,
|
||||
ItemStat = 9,
|
||||
|
||||
DroppedStack = 10,
|
||||
|
||||
MinValue = 0,
|
||||
MaxValue = 9
|
||||
MaxValue = 10
|
||||
}
|
||||
|
||||
public interface IEventData : NetEntityEvent.IData
|
||||
@@ -47,10 +50,12 @@ namespace Barotrauma
|
||||
{
|
||||
public EventType EventType => EventType.InventoryState;
|
||||
public readonly ItemContainer Component;
|
||||
public readonly Range SlotRange;
|
||||
|
||||
public InventoryStateEventData(ItemContainer component)
|
||||
public InventoryStateEventData(ItemContainer component, Range slotRange)
|
||||
{
|
||||
Component = component;
|
||||
SlotRange = slotRange;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +67,10 @@ namespace Barotrauma
|
||||
|
||||
public ChangePropertyEventData(SerializableProperty serializableProperty, ISerializableEntity entity)
|
||||
{
|
||||
if (serializableProperty.GetAttribute<Editable>() == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Attempted to create {nameof(ChangePropertyEventData)} for the non-editable property {serializableProperty.Name}.");
|
||||
}
|
||||
SerializableProperty = serializableProperty;
|
||||
Entity = entity;
|
||||
}
|
||||
@@ -94,6 +103,13 @@ namespace Barotrauma
|
||||
private readonly struct AssignCampaignInteractionEventData : IEventData
|
||||
{
|
||||
public EventType EventType => EventType.AssignCampaignInteraction;
|
||||
|
||||
public readonly ImmutableArray<Client> TargetClients;
|
||||
|
||||
public AssignCampaignInteractionEventData(IEnumerable<Client> targetClients)
|
||||
{
|
||||
TargetClients = (targetClients ?? Enumerable.Empty<Client>()).ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ApplyStatusEffectEventData : IEventData
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace Barotrauma
|
||||
if (itemPrefab == null) { return 0; }
|
||||
if (i < 0 || i >= slots.Length) { return 0; }
|
||||
if (!container.CanBeContained(itemPrefab, i)) { return 0; }
|
||||
return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.GetMaxStackSize(i)), condition);
|
||||
return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.GetMaxStackSize(this), container.GetMaxStackSize(i)), condition);
|
||||
}
|
||||
|
||||
public override bool IsFull(bool takeStacksIntoAccount = false)
|
||||
@@ -74,7 +74,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!slots[i].Any()) { return false; }
|
||||
var item = slots[i].FirstOrDefault();
|
||||
if (slots[i].Items.Count < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; }
|
||||
if (slots[i].Items.Count < Math.Min(item.Prefab.GetMaxStackSize(this), container.GetMaxStackSize(i))) { return false; }
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -133,7 +133,7 @@ namespace Barotrauma
|
||||
return wasPut;
|
||||
}
|
||||
|
||||
public override void CreateNetworkEvent()
|
||||
protected override void CreateNetworkEvent(Range slotRange)
|
||||
{
|
||||
if (!Item.ItemList.Contains(container.Item))
|
||||
{
|
||||
@@ -150,11 +150,17 @@ namespace Barotrauma
|
||||
DebugConsole.Log("Creating a network event for the item \"" + container.Item + "\" failed, ItemContainer not found in components");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (slotRange.Start.Value < 0 || slotRange.End.Value > capacity)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error when creating an inventory event: invalid slot range ({slotRange})\n" + Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; }
|
||||
GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new Item.InventoryStateEventData(container));
|
||||
GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new Item.InventoryStateEventData(container, slotRange));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,9 +43,9 @@ namespace Barotrauma
|
||||
public readonly bool CopyCondition;
|
||||
|
||||
//tag/identifier of the deconstructor(s) that can be used to deconstruct the item into this
|
||||
public readonly string[] RequiredDeconstructor;
|
||||
public readonly Identifier[] RequiredDeconstructor;
|
||||
//tag/identifier of other item(s) that that need to be present in the deconstructor to deconstruct the item into this
|
||||
public readonly string[] RequiredOtherItem;
|
||||
public readonly Identifier[] RequiredOtherItem;
|
||||
//text to display on the deconstructor's activate button when this output is available
|
||||
public readonly string ActivateButtonText;
|
||||
public readonly string InfoText;
|
||||
@@ -63,9 +63,9 @@ namespace Barotrauma
|
||||
OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f));
|
||||
CopyCondition = element.GetAttributeBool("copycondition", false);
|
||||
Commonness = element.GetAttributeFloat("commonness", 1.0f);
|
||||
RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor",
|
||||
element.Parent?.GetAttributeStringArray("requireddeconstructor", Array.Empty<string>()) ?? Array.Empty<string>());
|
||||
RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", Array.Empty<string>());
|
||||
RequiredDeconstructor = element.GetAttributeIdentifierArray("requireddeconstructor",
|
||||
element.Parent?.GetAttributeIdentifierArray("requireddeconstructor", Array.Empty<Identifier>()) ?? Array.Empty<Identifier>());
|
||||
RequiredOtherItem = element.GetAttributeIdentifierArray("requiredotheritem", Array.Empty<Identifier>());
|
||||
ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty);
|
||||
InfoText = element.GetAttributeString("infotext", string.Empty);
|
||||
InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty);
|
||||
@@ -88,20 +88,27 @@ namespace Barotrauma
|
||||
|
||||
public abstract ItemPrefab FirstMatchingPrefab { get; }
|
||||
|
||||
public LocalizedString OverrideHeader { get; }
|
||||
public LocalizedString OverrideDescription { get; }
|
||||
|
||||
public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription)
|
||||
public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem)
|
||||
{
|
||||
Amount = amount;
|
||||
MinCondition = minCondition;
|
||||
MaxCondition = maxCondition;
|
||||
UseCondition = useCondition;
|
||||
OverrideHeader = overrideHeader;
|
||||
OverrideDescription = overrideDescription;
|
||||
DefaultItem = defaultItem;
|
||||
}
|
||||
public readonly int Amount;
|
||||
public readonly float MinCondition;
|
||||
public readonly float MaxCondition;
|
||||
public readonly bool UseCondition;
|
||||
/// <summary>
|
||||
/// Used only when there's multiple optional items.
|
||||
/// </summary>
|
||||
public readonly Identifier DefaultItem;
|
||||
|
||||
public bool IsConditionSuitable(float conditionPercentage)
|
||||
{
|
||||
@@ -138,8 +145,8 @@ namespace Barotrauma
|
||||
return item?.Prefab.Identifier == ItemPrefabIdentifier;
|
||||
}
|
||||
|
||||
public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) :
|
||||
base(amount, minCondition, maxCondition, useCondition, overrideDescription)
|
||||
public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader) :
|
||||
base(amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem: Identifier.Empty)
|
||||
{
|
||||
ItemPrefabIdentifier = itemPrefab;
|
||||
using MD5 md5 = MD5.Create();
|
||||
@@ -158,9 +165,26 @@ namespace Barotrauma
|
||||
|
||||
public override UInt32 UintIdentifier { get; }
|
||||
|
||||
public override IEnumerable<ItemPrefab> ItemPrefabs => ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag));
|
||||
private readonly List<ItemPrefab> cachedPrefabs = new List<ItemPrefab>();
|
||||
|
||||
public override ItemPrefab FirstMatchingPrefab => ItemPrefab.Prefabs.FirstOrDefault(p => p.Tags.Contains(Tag));
|
||||
private Md5Hash prevContentPackagesHash;
|
||||
|
||||
public override IEnumerable<ItemPrefab> ItemPrefabs
|
||||
{
|
||||
get
|
||||
{
|
||||
if (prevContentPackagesHash == null ||
|
||||
!prevContentPackagesHash.Equals(ContentPackageManager.EnabledPackages.MergedHash))
|
||||
{
|
||||
cachedPrefabs.Clear();
|
||||
cachedPrefabs.AddRange(ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag)));
|
||||
prevContentPackagesHash = ContentPackageManager.EnabledPackages.MergedHash;
|
||||
}
|
||||
return cachedPrefabs;
|
||||
}
|
||||
}
|
||||
|
||||
public override ItemPrefab FirstMatchingPrefab => ItemPrefabs.FirstOrDefault();
|
||||
|
||||
public override bool MatchesItem(Item item)
|
||||
{
|
||||
@@ -168,8 +192,8 @@ namespace Barotrauma
|
||||
return item.HasTag(Tag);
|
||||
}
|
||||
|
||||
public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription)
|
||||
: base(amount, minCondition, maxCondition, useCondition, overrideDescription)
|
||||
public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem)
|
||||
: base(amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem)
|
||||
{
|
||||
Tag = tag;
|
||||
using MD5 md5 = MD5.Create();
|
||||
@@ -186,7 +210,7 @@ namespace Barotrauma
|
||||
public ItemPrefab TargetItem => ItemPrefab.Prefabs[TargetItemPrefabIdentifier];
|
||||
|
||||
private readonly Lazy<LocalizedString> displayName;
|
||||
public LocalizedString DisplayName
|
||||
public LocalizedString DisplayName
|
||||
=> ItemPrefab.Prefabs.ContainsKey(TargetItemPrefabIdentifier) ? displayName.Value : "";
|
||||
public readonly ImmutableArray<RequiredItem> RequiredItems;
|
||||
public readonly ImmutableArray<Identifier> SuitableFabricatorIdentifiers;
|
||||
@@ -198,6 +222,7 @@ namespace Barotrauma
|
||||
public readonly uint RecipeHash;
|
||||
public readonly int Amount;
|
||||
public readonly int? Quality;
|
||||
public readonly bool HideForNonTraitors;
|
||||
|
||||
/// <summary>
|
||||
/// How many of this item the fabricator can create (< 0 = unlimited)
|
||||
@@ -230,6 +255,8 @@ namespace Barotrauma
|
||||
FabricationLimitMin = element.GetAttributeInt(nameof(FabricationLimitMin), limitDefault);
|
||||
FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault);
|
||||
|
||||
HideForNonTraitors = element.GetAttributeBool(nameof(HideForNonTraitors), false);
|
||||
|
||||
if (element.GetAttribute(nameof(Quality)) != null)
|
||||
{
|
||||
Quality = element.GetAttributeInt(nameof(Quality), 0);
|
||||
@@ -266,10 +293,15 @@ namespace Barotrauma
|
||||
bool useCondition = subElement.GetAttributeBool("usecondition", true);
|
||||
int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1));
|
||||
|
||||
LocalizedString description = string.Empty;
|
||||
if (subElement.GetAttributeString("description", string.Empty) is string texTag && !texTag.IsNullOrEmpty())
|
||||
LocalizedString overrideDescription = string.Empty;
|
||||
if (subElement.GetAttributeString("description", string.Empty) is string descriptionTag && !descriptionTag.IsNullOrEmpty())
|
||||
{
|
||||
description = TextManager.Get(texTag);
|
||||
overrideDescription = TextManager.Get(descriptionTag);
|
||||
}
|
||||
LocalizedString overrideHeader = string.Empty;
|
||||
if (subElement.GetAttributeString("header", string.Empty) is string headerTag && !headerTag.IsNullOrEmpty())
|
||||
{
|
||||
overrideHeader = TextManager.Get(headerTag);
|
||||
}
|
||||
|
||||
if (requiredItemIdentifier != Identifier.Empty)
|
||||
@@ -284,7 +316,7 @@ namespace Barotrauma
|
||||
amount += requiredItems[existing].Amount;
|
||||
requiredItems.RemoveAt(existing);
|
||||
}
|
||||
requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, description));
|
||||
requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -298,7 +330,8 @@ namespace Barotrauma
|
||||
amount += requiredItems[existing].Amount;
|
||||
requiredItems.RemoveAt(existing);
|
||||
}
|
||||
requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, description));
|
||||
Identifier defaultItem = subElement.GetAttributeIdentifier("defaultitem", Identifier.Empty);
|
||||
requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -316,12 +349,13 @@ namespace Barotrauma
|
||||
uint outputId = ToolBox.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5);
|
||||
|
||||
var requiredItems = string.Join(':', RequiredItems
|
||||
.Select(i => i.UintIdentifier)
|
||||
.Select(i => string.Join(',', i)));
|
||||
.Select(static i => $"{i.UintIdentifier}:{i.Amount}")
|
||||
.Select(static i => string.Join(',', i)));
|
||||
// above but include the item amount
|
||||
|
||||
var requiredSkills = string.Join(':', RequiredSkills.Select(s => $"{s.Identifier}:{s.Level}"));
|
||||
|
||||
uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{requiredItems}|{requiredSkills}", md5);
|
||||
uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{RequiresRecipe}|{requiredItems}|{requiredSkills}", md5);
|
||||
if (retVal == 0) { retVal = 1; }
|
||||
return retVal;
|
||||
}
|
||||
@@ -681,6 +715,9 @@ namespace Barotrauma
|
||||
[Serialize(0.0f, IsPropertySaveable.No)]
|
||||
public float OffsetOnSelected { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Should the character who's selected the item grab it (hold their hand on it, the same way as they do when repairing)? Defaults to true on items that have an ItemContainer component.")]
|
||||
public bool GrabWhenSelected { get; set; }
|
||||
|
||||
private float health;
|
||||
|
||||
[Serialize(100.0f, IsPropertySaveable.No)]
|
||||
@@ -749,7 +786,7 @@ namespace Barotrauma
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
public bool DisableItemUsageWhenSelected { get; private set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.No)]
|
||||
[Serialize("metalcrate", IsPropertySaveable.No)]
|
||||
public string CargoContainerIdentifier { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
@@ -802,7 +839,40 @@ namespace Barotrauma
|
||||
public int MaxStackSize
|
||||
{
|
||||
get { return maxStackSize; }
|
||||
private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); }
|
||||
private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxPossibleStackSize); }
|
||||
}
|
||||
|
||||
private int maxStackSizeCharacterInventory;
|
||||
[Serialize(-1, IsPropertySaveable.No)]
|
||||
public int MaxStackSizeCharacterInventory
|
||||
{
|
||||
get { return maxStackSizeCharacterInventory; }
|
||||
private set { maxStackSizeCharacterInventory = Math.Min(value, Inventory.MaxPossibleStackSize); }
|
||||
}
|
||||
|
||||
private int maxStackSizeHoldableOrWearableInventory;
|
||||
[Serialize(-1, IsPropertySaveable.No)]
|
||||
public int MaxStackSizeHoldableOrWearableInventory
|
||||
{
|
||||
get { return maxStackSizeHoldableOrWearableInventory; }
|
||||
private set { maxStackSizeHoldableOrWearableInventory = Math.Min(value, Inventory.MaxPossibleStackSize); }
|
||||
}
|
||||
|
||||
public int GetMaxStackSize(Inventory inventory)
|
||||
{
|
||||
if (inventory is CharacterInventory && maxStackSizeCharacterInventory > 0)
|
||||
{
|
||||
return maxStackSizeCharacterInventory;
|
||||
}
|
||||
else if (maxStackSizeHoldableOrWearableInventory > 0 &&
|
||||
inventory?.Owner is Item item && (item.GetComponent<Holdable>() != null || item.GetComponent<Wearable>() != null))
|
||||
{
|
||||
return maxStackSizeHoldableOrWearableInventory;
|
||||
}
|
||||
else
|
||||
{
|
||||
return maxStackSize;
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
@@ -830,13 +900,19 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")]
|
||||
public float AISlowTurretPriority { get; private set; }
|
||||
|
||||
|
||||
[Serialize(float.PositiveInfinity, IsPropertySaveable.No, description: "The max distance at which the bots are allowed to target the items. Defaults to infinity.")]
|
||||
public float AITurretTargetingMaxDistance { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, taking items from this container is never considered stealing.")]
|
||||
public bool AllowStealingContainedItems { get; private set; }
|
||||
|
||||
[Serialize("255,255,255,255", IsPropertySaveable.No, description: "Used in circuit box to set the color of the nodes.")]
|
||||
public Color SignalComponentColor { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "If enabled, the player is unable to open the middle click menu when this item is selected.")]
|
||||
public bool DisableCommandMenuWhenSelected { get; set; }
|
||||
|
||||
protected override Identifier DetermineIdentifier(XElement element)
|
||||
{
|
||||
Identifier identifier = base.DetermineIdentifier(element);
|
||||
@@ -1040,7 +1116,7 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
$"Error in item prefab \"{ToString()}\": " +
|
||||
$"{prevRecipe.DisplayName} has the same hash as {newRecipe.DisplayName}. " +
|
||||
$"{prevRecipe.TargetItemPrefabIdentifier} has the same hash as {newRecipe.TargetItemPrefabIdentifier}. " +
|
||||
$"This will cause issues with fabrication."
|
||||
);
|
||||
}
|
||||
@@ -1172,6 +1248,11 @@ namespace Barotrauma
|
||||
#endif
|
||||
|
||||
this.allowedLinks = ConfigElement.GetAttributeIdentifierArray("allowedlinks", Array.Empty<Identifier>()).ToImmutableHashSet();
|
||||
|
||||
GrabWhenSelected = ConfigElement.GetAttributeBool(
|
||||
nameof(GrabWhenSelected),
|
||||
ConfigElement.GetChildElement(nameof(ItemContainer)) != null &&
|
||||
ConfigElement.GetChildElement("Body") == null);
|
||||
}
|
||||
|
||||
public CommonnessInfo? GetCommonnessInfo(Level level)
|
||||
|
||||
@@ -43,21 +43,24 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should an empty inventory be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it.
|
||||
/// Should an empty inventory (or an empty inventory slot if <see cref="TargetSlot"/> is set) be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it.
|
||||
/// </summary>
|
||||
public bool MatchOnEmpty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should only an empty inventory be considered valid? Can be used to, for example, make an item do something when there's nothing inside it.
|
||||
/// Should only an empty inventory (or an empty inventory slot if <see cref="TargetSlot"/> is set) be considered valid? Can be used to, for example, make an item do something when there's nothing inside it.
|
||||
/// </summary>
|
||||
public bool RequireEmpty { get; set; }
|
||||
|
||||
private bool RequireOrMatchOnEmpty => MatchOnEmpty || RequireEmpty;
|
||||
|
||||
/// <summary>
|
||||
/// Only valid for the RequiredItems of an ItemComponent. Can be used to ignore the requirement in the submarine editor,
|
||||
/// making it easier to for example make rewire things that require some special tool to rewire.
|
||||
/// </summary>
|
||||
public bool IgnoreInEditor { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Identifier(s) or tag(s) of the items that are NOT considered valid.
|
||||
/// Can be used to, for example, exclude some specific items when using tags that apply to multiple items.
|
||||
@@ -103,6 +106,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public int TargetSlot = -1;
|
||||
|
||||
/// <summary>
|
||||
/// The slot type the target must be in when targeting an item contained inside a character's inventory
|
||||
/// </summary>
|
||||
public InvSlotType CharacterInventorySlotType;
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the position defined in ItemContainer. Only valid when used in the Containable definitions of an ItemContainer.
|
||||
/// </summary>
|
||||
@@ -167,6 +175,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (item.HasTag(excludedIdentifier)) { return false; }
|
||||
}
|
||||
if (item.ParentInventory?.Owner is Character character && CharacterInventorySlotType != InvSlotType.None)
|
||||
{
|
||||
if (!character.HasEquippedItem(item, CharacterInventorySlotType)) { return false; }
|
||||
}
|
||||
if (Identifiers.Contains(item.Prefab.Identifier)) { return true; }
|
||||
foreach (var identifier in Identifiers)
|
||||
{
|
||||
@@ -261,6 +273,8 @@ namespace Barotrauma
|
||||
Rotation = element.GetAttributeFloat("rotation", 0f);
|
||||
SetActive = element.GetAttributeBool("setactive", false);
|
||||
|
||||
CharacterInventorySlotType = element.GetAttributeEnum(nameof(CharacterInventorySlotType), InvSlotType.None);
|
||||
|
||||
if (element.GetAttribute(nameof(Hide)) != null)
|
||||
{
|
||||
Hide = element.GetAttributeBool(nameof(Hide), false);
|
||||
@@ -332,7 +346,7 @@ namespace Barotrauma
|
||||
case RelationType.Equipped:
|
||||
if (character == null) { return false; }
|
||||
var heldItems = character.HeldItems;
|
||||
if ((RequireEmpty || MatchOnEmpty) && heldItems.None()) { return true; }
|
||||
if (RequireOrMatchOnEmpty && heldItems.None()) { return true; }
|
||||
foreach (Item equippedItem in heldItems)
|
||||
{
|
||||
if (equippedItem == null) { continue; }
|
||||
@@ -347,7 +361,7 @@ namespace Barotrauma
|
||||
if (character == null) { return false; }
|
||||
if (character.Inventory == null) { return MatchOnEmpty || RequireEmpty; }
|
||||
var allItems = character.Inventory.AllItems;
|
||||
if ((RequireEmpty || MatchOnEmpty) && allItems.None()) { return true; }
|
||||
if (RequireOrMatchOnEmpty && allItems.None()) { return true; }
|
||||
foreach (Item pickedItem in allItems)
|
||||
{
|
||||
if (pickedItem == null) { continue; }
|
||||
@@ -370,11 +384,21 @@ namespace Barotrauma
|
||||
private bool CheckContained(Item parentItem)
|
||||
{
|
||||
if (parentItem.OwnInventory == null) { return false; }
|
||||
bool isEmpty = parentItem.OwnInventory.IsEmpty();
|
||||
if (RequireEmpty && !isEmpty) { return false; }
|
||||
if (MatchOnEmpty && isEmpty) { return true; }
|
||||
|
||||
if (TargetSlot == -1 && RequireOrMatchOnEmpty)
|
||||
{
|
||||
bool isEmpty = parentItem.OwnInventory.IsEmpty();
|
||||
if (RequireEmpty) { return isEmpty; }
|
||||
if (MatchOnEmpty && isEmpty) { return true; }
|
||||
}
|
||||
foreach (var container in parentItem.GetComponents<Items.Components.ItemContainer>())
|
||||
{
|
||||
if (TargetSlot > -1 && RequireOrMatchOnEmpty)
|
||||
{
|
||||
var itemInSlot = container.Inventory.GetItemAt(TargetSlot);
|
||||
if (RequireEmpty) { return itemInSlot == null; }
|
||||
if (MatchOnEmpty && itemInSlot == null) { return true; }
|
||||
}
|
||||
foreach (Item contained in container.Inventory.AllItems)
|
||||
{
|
||||
if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; }
|
||||
@@ -389,7 +413,8 @@ namespace Barotrauma
|
||||
{
|
||||
element.Add(
|
||||
new XAttribute("items", JoinedIdentifiers),
|
||||
new XAttribute("type", type.ToString()),
|
||||
new XAttribute("type", type.ToString()),
|
||||
new XAttribute("characterinventoryslottype", CharacterInventorySlotType.ToString()),
|
||||
new XAttribute("optional", IsOptional),
|
||||
new XAttribute("ignoreineditor", IgnoreInEditor),
|
||||
new XAttribute("excludebroken", ExcludeBroken),
|
||||
|
||||
@@ -9,11 +9,13 @@ namespace Barotrauma
|
||||
{
|
||||
public Identifier Item;
|
||||
public int Amount;
|
||||
|
||||
public bool MultiPlayerOnly;
|
||||
|
||||
public StartItem(XElement element)
|
||||
{
|
||||
Item = element.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
Amount = element.GetAttributeInt("amount", 1);
|
||||
Amount = element.GetAttributeInt(nameof(Amount), 1);
|
||||
MultiPlayerOnly = element.GetAttributeBool(nameof(MultiPlayerOnly), false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user