Unstable 1.1.14.0
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user