(61d00a474) v0.9.7.1
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
using FarseerPhysics;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
struct LimbPos
|
||||
{
|
||||
public LimbType limbType;
|
||||
public Vector2 position;
|
||||
|
||||
public LimbPos(LimbType limbType, Vector2 position)
|
||||
{
|
||||
this.limbType = limbType;
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
partial class Controller : ItemComponent, IServerSerializable
|
||||
{
|
||||
//where the limbs of the user should be positioned when using the controller
|
||||
private readonly List<LimbPos> limbPositions;
|
||||
|
||||
private Direction dir;
|
||||
|
||||
//the position where the user walks to when using the controller
|
||||
//(relative to the position of the item)
|
||||
private Vector2 userPos;
|
||||
|
||||
private Camera cam;
|
||||
|
||||
private Character user;
|
||||
|
||||
private Item focusTarget;
|
||||
private float targetRotation;
|
||||
|
||||
private bool state;
|
||||
|
||||
public Vector2 UserPos
|
||||
{
|
||||
get { return userPos; }
|
||||
set { userPos = value; }
|
||||
}
|
||||
|
||||
public Character User
|
||||
{
|
||||
get { return user; }
|
||||
}
|
||||
|
||||
public IEnumerable<LimbPos> LimbPositions { get { return limbPositions; } }
|
||||
|
||||
[Editable, Serialize(false, false, 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.")]
|
||||
public bool IsToggle
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Controller(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
limbPositions = new List<LimbPos>();
|
||||
|
||||
userPos = element.GetAttributeVector2("UserPos", Vector2.Zero);
|
||||
|
||||
Enum.TryParse(element.GetAttributeString("direction", "None"), out dir);
|
||||
|
||||
foreach (XElement el in element.Elements())
|
||||
{
|
||||
if (el.Name != "limbposition") continue;
|
||||
|
||||
LimbPos lp = new LimbPos();
|
||||
|
||||
try
|
||||
{
|
||||
lp.limbType = (LimbType)Enum.Parse(typeof(LimbType), el.Attribute("limb").Value, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in " + element + ": " + e.Message, e);
|
||||
}
|
||||
|
||||
lp.position = el.GetAttributeVector2("position", Vector2.Zero);
|
||||
|
||||
limbPositions.Add(lp);
|
||||
}
|
||||
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
this.cam = cam;
|
||||
|
||||
if (IsToggle)
|
||||
{
|
||||
item.SendSignal(0, state ? "1" : "0", "signal_out", sender: null);
|
||||
}
|
||||
|
||||
if (user == null
|
||||
|| user.Removed
|
||||
|| user.SelectedConstruction != item
|
||||
|| !user.CanInteractWith(item))
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
CancelUsing(user);
|
||||
user = null;
|
||||
}
|
||||
if (!IsToggle) { IsActive = false; }
|
||||
return;
|
||||
}
|
||||
|
||||
user.AnimController.Anim = AnimController.Animation.UsingConstruction;
|
||||
|
||||
if (userPos != Vector2.Zero)
|
||||
{
|
||||
Vector2 diff = (item.WorldPosition + userPos) - user.WorldPosition;
|
||||
|
||||
if (user.AnimController.InWater)
|
||||
{
|
||||
if (diff.LengthSquared() > 30.0f * 30.0f)
|
||||
{
|
||||
user.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One);
|
||||
user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
|
||||
}
|
||||
else
|
||||
{
|
||||
user.AnimController.TargetMovement = Vector2.Zero;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
diff.Y = 0.0f;
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && user != Character.Controlled)
|
||||
{
|
||||
if (Math.Abs(diff.X) > 20.0f)
|
||||
{
|
||||
//wait for the character to walk to the correct position
|
||||
return;
|
||||
}
|
||||
else if (Math.Abs(diff.X) > 0.1f)
|
||||
{
|
||||
//aim to keep the collider at the correct position once close enough
|
||||
user.AnimController.Collider.LinearVelocity = new Vector2(
|
||||
diff.X * 0.1f,
|
||||
user.AnimController.Collider.LinearVelocity.Y);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Math.Abs(diff.X) > 10.0f)
|
||||
{
|
||||
user.AnimController.TargetMovement = Vector2.Normalize(diff);
|
||||
user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
|
||||
return;
|
||||
}
|
||||
}
|
||||
user.AnimController.TargetMovement = Vector2.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, user);
|
||||
|
||||
if (limbPositions.Count == 0) { return; }
|
||||
|
||||
user.AnimController.Anim = AnimController.Animation.UsingConstruction;
|
||||
|
||||
user.AnimController.ResetPullJoints();
|
||||
|
||||
if (dir != 0) user.AnimController.TargetDir = dir;
|
||||
|
||||
foreach (LimbPos lb in limbPositions)
|
||||
{
|
||||
Limb limb = user.AnimController.GetLimb(lb.limbType);
|
||||
if (limb == null || !limb.body.Enabled) continue;
|
||||
|
||||
limb.Disabled = true;
|
||||
|
||||
Vector2 worldPosition = new Vector2(item.WorldRect.X, item.WorldRect.Y) + lb.position * item.Scale;
|
||||
Vector2 diff = worldPosition - limb.WorldPosition;
|
||||
|
||||
limb.PullJointEnabled = true;
|
||||
limb.PullJointWorldAnchorB = limb.SimPosition + ConvertUnits.ToSimUnits(diff);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Use(float deltaTime, Character activator = null)
|
||||
{
|
||||
if (activator != user)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user == null || user.Removed ||
|
||||
user.SelectedConstruction != item || !user.CanInteractWith(item))
|
||||
{
|
||||
user = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
item.SendSignal(0, "1", "trigger_out", user);
|
||||
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, activator);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool SecondaryUse(float deltaTime, Character character = null)
|
||||
{
|
||||
if (this.user != character)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.user == null || character.Removed ||
|
||||
this.user.SelectedConstruction != item || !character.CanInteractWith(item))
|
||||
{
|
||||
this.user = null;
|
||||
return false;
|
||||
}
|
||||
if (character == null) return false;
|
||||
|
||||
focusTarget = GetFocusTarget();
|
||||
if (focusTarget == null)
|
||||
{
|
||||
Vector2 centerPos = new Vector2(item.WorldRect.Center.X, item.WorldRect.Center.Y);
|
||||
|
||||
Vector2 offset = character.CursorWorldPosition - centerPos;
|
||||
offset.Y = -offset.Y;
|
||||
|
||||
targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset));
|
||||
return false;
|
||||
}
|
||||
|
||||
character.ViewTarget = focusTarget;
|
||||
#if CLIENT
|
||||
if (character == Character.Controlled && cam != null)
|
||||
{
|
||||
Lights.LightManager.ViewTarget = focusTarget;
|
||||
cam.TargetPos = focusTarget.WorldPosition;
|
||||
|
||||
cam.OffsetAmount = MathHelper.Lerp(cam.OffsetAmount, (focusTarget as Item).Prefab.OffsetOnSelected, deltaTime * 10.0f);
|
||||
HideHUDs(true);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!character.IsRemotePlayer || character.ViewTarget == focusTarget)
|
||||
{
|
||||
Vector2 centerPos = new Vector2(item.WorldRect.Center.X, item.WorldRect.Center.Y);
|
||||
|
||||
Item targetItem = focusTarget as Item;
|
||||
if (targetItem != null)
|
||||
{
|
||||
Turret turret = targetItem.GetComponent<Turret>();
|
||||
if (turret != null)
|
||||
{
|
||||
centerPos = new Vector2(targetItem.WorldRect.X + turret.TransformedBarrelPos.X, targetItem.WorldRect.Y - turret.TransformedBarrelPos.Y);
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 offset = character.CursorWorldPosition - centerPos;
|
||||
offset.Y = -offset.Y;
|
||||
|
||||
targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Item GetFocusTarget()
|
||||
{
|
||||
item.SendSignal(0, MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), "position_out", user);
|
||||
|
||||
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (item.LastSentSignalRecipients[i].Condition <= 0.0f) continue;
|
||||
if (item.LastSentSignalRecipients[i].Prefab.FocusOnSelected)
|
||||
{
|
||||
return item.LastSentSignalRecipients[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
if (IsToggle)
|
||||
{
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
state = !state;
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
item.SendSignal(0, "1", "signal_out", picker);
|
||||
}
|
||||
#if CLIENT
|
||||
PlaySound(ActionType.OnUse, picker);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CancelUsing(Character character)
|
||||
{
|
||||
if (character == null || character.Removed) { return; }
|
||||
|
||||
foreach (LimbPos lb in limbPositions)
|
||||
{
|
||||
Limb limb = character.AnimController.GetLimb(lb.limbType);
|
||||
if (limb == null) continue;
|
||||
|
||||
limb.Disabled = false;
|
||||
limb.PullJointEnabled = false;
|
||||
}
|
||||
|
||||
if (character.SelectedConstruction == this.item) { character.SelectedConstruction = null; }
|
||||
|
||||
character.AnimController.Anim = AnimController.Animation.None;
|
||||
if (character == Character.Controlled)
|
||||
{
|
||||
HideHUDs(false);
|
||||
}
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
public override bool Select(Character activator)
|
||||
{
|
||||
if (activator == null || activator.Removed) { return false; }
|
||||
|
||||
//someone already using the item
|
||||
if (user != null && !user.Removed)
|
||||
{
|
||||
if (user == activator)
|
||||
{
|
||||
IsActive = false;
|
||||
CancelUsing(user);
|
||||
user = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
user = activator;
|
||||
IsActive = true;
|
||||
}
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
item.SendSignal(0, "1", "signal_out", user);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void FlipX(bool relativeToSub)
|
||||
{
|
||||
if (dir != Direction.None)
|
||||
{
|
||||
dir = dir == Direction.Left ? Direction.Right : Direction.Left;
|
||||
}
|
||||
|
||||
userPos.X = -UserPos.X;
|
||||
|
||||
for (int i = 0; i < limbPositions.Count; i++)
|
||||
{
|
||||
float diff = (item.Rect.X + limbPositions[i].position.X * item.Scale) - item.Rect.Center.X;
|
||||
|
||||
Vector2 flippedPos =
|
||||
new Vector2(
|
||||
(item.Rect.Center.X - diff - item.Rect.X) / item.Scale,
|
||||
limbPositions[i].position.Y);
|
||||
|
||||
limbPositions[i] = new LimbPos(limbPositions[i].limbType, flippedPos);
|
||||
}
|
||||
}
|
||||
|
||||
public override void FlipY(bool relativeToSub)
|
||||
{
|
||||
userPos.Y = -UserPos.Y;
|
||||
|
||||
for (int i = 0; i < limbPositions.Count; i++)
|
||||
{
|
||||
float diff = (item.Rect.Y + limbPositions[i].position.Y) - item.Rect.Center.Y;
|
||||
|
||||
Vector2 flippedPos =
|
||||
new Vector2(
|
||||
limbPositions[i].position.X,
|
||||
item.Rect.Center.Y - diff - item.Rect.Y);
|
||||
|
||||
limbPositions[i] = new LimbPos(limbPositions[i].limbType, flippedPos);
|
||||
}
|
||||
}
|
||||
|
||||
partial void HideHUDs(bool value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Deconstructor : Powered, IServerSerializable, IClientSerializable
|
||||
{
|
||||
private float progressTimer;
|
||||
private float progressState;
|
||||
|
||||
private bool hasPower;
|
||||
|
||||
private ItemContainer inputContainer, outputContainer;
|
||||
|
||||
public ItemContainer InputContainer
|
||||
{
|
||||
get { return inputContainer; }
|
||||
}
|
||||
|
||||
public ItemContainer OutputContainer
|
||||
{
|
||||
get { return outputContainer; }
|
||||
}
|
||||
|
||||
public Deconstructor(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
var containers = item.GetComponents<ItemContainer>().ToList();
|
||||
if (containers.Count < 2)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item \"" + item.Name + "\": Deconstructors must have two ItemContainer components!");
|
||||
return;
|
||||
}
|
||||
|
||||
inputContainer = containers[0];
|
||||
outputContainer = containers[1];
|
||||
|
||||
OnItemLoadedProjSpecific();
|
||||
}
|
||||
|
||||
partial void OnItemLoadedProjSpecific();
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
MoveInputQueue();
|
||||
|
||||
if (inputContainer == null || inputContainer.Inventory.Items.All(i => i == null))
|
||||
{
|
||||
SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
hasPower = Voltage >= MinVoltage;
|
||||
if (!hasPower) { return; }
|
||||
|
||||
var repairable = item.GetComponent<Repairable>();
|
||||
if (repairable != null)
|
||||
{
|
||||
repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f;
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
|
||||
if (powerConsumption <= 0.0f) { Voltage = 1.0f; }
|
||||
progressTimer += deltaTime * Math.Min(Voltage, 1.0f);
|
||||
|
||||
var targetItem = inputContainer.Inventory.Items.LastOrDefault(i => i != null);
|
||||
if (targetItem == null) { return; }
|
||||
|
||||
float deconstructTime = targetItem.Prefab.DeconstructItems.Any() ? targetItem.Prefab.DeconstructTime : 1.0f;
|
||||
|
||||
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
|
||||
if (progressTimer > deconstructTime)
|
||||
{
|
||||
int emptySlots = outputContainer.Inventory.Items.Where(i => i == null).Count();
|
||||
|
||||
foreach (DeconstructItem deconstructProduct in targetItem.Prefab.DeconstructItems)
|
||||
{
|
||||
float percentageHealth = targetItem.Condition / targetItem.Prefab.Health;
|
||||
if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) continue;
|
||||
|
||||
if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab))
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!");
|
||||
continue;
|
||||
}
|
||||
|
||||
float condition = deconstructProduct.CopyCondition ?
|
||||
percentageHealth * itemPrefab.Health :
|
||||
itemPrefab.Health * deconstructProduct.OutCondition;
|
||||
|
||||
//container full, drop the items outside the deconstructor
|
||||
if (emptySlots <= 0)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(itemPrefab, item.Position, item.Submarine, condition);
|
||||
}
|
||||
else
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition);
|
||||
emptySlots--;
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
if (targetItem.Prefab.DeconstructItems.Any())
|
||||
{
|
||||
//drop all items that are inside the deconstructed item
|
||||
foreach (ItemContainer ic in targetItem.GetComponents<ItemContainer>())
|
||||
{
|
||||
if (ic?.Inventory?.Items == null) { continue; }
|
||||
foreach (Item containedItem in ic.Inventory.Items)
|
||||
{
|
||||
containedItem?.Drop(dropper: null, createNetworkEvent: true);
|
||||
}
|
||||
}
|
||||
|
||||
inputContainer.Inventory.RemoveItem(targetItem);
|
||||
Entity.Spawner.AddToRemoveQueue(targetItem);
|
||||
MoveInputQueue();
|
||||
PutItemsToLinkedContainer();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (outputContainer.Inventory.Items.All(i => i != null))
|
||||
{
|
||||
targetItem.Drop(dropper: null);
|
||||
}
|
||||
else
|
||||
{
|
||||
outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true);
|
||||
}
|
||||
}
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
progressTimer = 0.0f;
|
||||
progressState = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PutItemsToLinkedContainer()
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
if (outputContainer.Inventory.Items.All(it => it == null)) return;
|
||||
|
||||
foreach (MapEntity linkedTo in item.linkedTo)
|
||||
{
|
||||
if (linkedTo is Item linkedItem)
|
||||
{
|
||||
var fabricator = linkedItem.GetComponent<Fabricator>();
|
||||
if (fabricator != null) { continue; }
|
||||
var itemContainer = linkedItem.GetComponent<ItemContainer>();
|
||||
if (itemContainer == null) { continue; }
|
||||
|
||||
foreach (Item containedItem in outputContainer.Inventory.Items)
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (itemContainer.Inventory.Items.All(it => it != null)) { break; }
|
||||
itemContainer.Inventory.TryPutItem(containedItem, user: null, createNetworkEvent: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move items towards the last slot in the inventory if there's free slots
|
||||
/// </summary>
|
||||
private void MoveInputQueue()
|
||||
{
|
||||
for (int i = inputContainer.Inventory.Capacity - 2; i >= 0; i--)
|
||||
{
|
||||
if (inputContainer.Inventory.Items[i] != null && inputContainer.Inventory.Items[i + 1] == null)
|
||||
{
|
||||
inputContainer.Inventory.TryPutItem(inputContainer.Inventory.Items[i], i + 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetActive(bool active, Character user = null)
|
||||
{
|
||||
PutItemsToLinkedContainer();
|
||||
|
||||
if (inputContainer.Inventory.Items.All(i => i == null)) { active = false; }
|
||||
|
||||
IsActive = active;
|
||||
currPowerConsumption = IsActive ? powerConsumption : 0.0f;
|
||||
|
||||
#if SERVER
|
||||
if (user != null)
|
||||
{
|
||||
GameServer.Log(user.LogName + (IsActive ? " activated " : " deactivated ") + item.Name, ServerLog.MessageType.ItemInteraction);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!IsActive)
|
||||
{
|
||||
progressTimer = 0.0f;
|
||||
progressState = 0.0f;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
activateButton.Text = TextManager.Get(IsActive ? "DeconstructorCancel" : "DeconstructorDeconstruct");
|
||||
#endif
|
||||
|
||||
inputContainer.Inventory.Locked = IsActive;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Engine : Powered, IServerSerializable, IClientSerializable
|
||||
{
|
||||
private float force;
|
||||
|
||||
private float targetForce;
|
||||
|
||||
private float maxForce;
|
||||
|
||||
private Attack propellerDamage;
|
||||
|
||||
private float damageTimer;
|
||||
|
||||
private bool hasPower;
|
||||
|
||||
private float prevVoltage;
|
||||
|
||||
private float controlLockTimer;
|
||||
|
||||
[Editable(0.0f, 10000000.0f),
|
||||
Serialize(2000.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")]
|
||||
public float MaxForce
|
||||
{
|
||||
get { return maxForce; }
|
||||
set
|
||||
{
|
||||
maxForce = Math.Max(0.0f, value);
|
||||
}
|
||||
}
|
||||
|
||||
[Editable, Serialize("0.0,0.0", true,
|
||||
description: "The position of the propeller as an offset from the item's center (in pixels)."+
|
||||
" Determines where the particles spawn and the position that causes characters to take damage from the engine if the PropellerDamage is defined.")]
|
||||
public Vector2 PropellerPos
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public float Force
|
||||
{
|
||||
get { return force;}
|
||||
set { force = MathHelper.Clamp(value, -100.0f, 100.0f); }
|
||||
}
|
||||
|
||||
public float CurrentVolume
|
||||
{
|
||||
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); }
|
||||
}
|
||||
|
||||
public Engine(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "propellerdamage":
|
||||
propellerDamage = new Attack(subElement, item.Name + ", Engine");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
UpdateOnActiveEffects(deltaTime);
|
||||
|
||||
UpdateAnimation(deltaTime);
|
||||
|
||||
controlLockTimer -= deltaTime;
|
||||
|
||||
currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption;
|
||||
//pumps consume more power when in a bad condition
|
||||
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
if (powerConsumption == 0.0f) { Voltage = 1.0f; }
|
||||
|
||||
prevVoltage = Voltage;
|
||||
hasPower = Voltage > MinVoltage;
|
||||
|
||||
Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, 0.1f);
|
||||
if (Math.Abs(Force) > 1.0f)
|
||||
{
|
||||
//arbitrary multiplier that was added to changes in submarine mass without having to readjust all engines
|
||||
float forceMultiplier = 0.1f;
|
||||
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage / MinVoltage, 1.0f);
|
||||
Vector2 currForce = new Vector2(force * maxForce * forceMultiplier * voltageFactor, 0.0f);
|
||||
//less effective when in a bad condition
|
||||
currForce *= MathHelper.Lerp(0.5f, 2.0f, item.Condition / item.MaxCondition);
|
||||
item.Submarine.ApplyForce(currForce);
|
||||
UpdatePropellerDamage(deltaTime);
|
||||
float maxChangeSpeed = 0.5f;
|
||||
float modifier = 2;
|
||||
float noise = currForce.Length() * forceMultiplier * modifier / maxForce;
|
||||
float min = Math.Max(1 - maxChangeSpeed, 0);
|
||||
float max = 1 + maxChangeSpeed;
|
||||
UpdateAITargets(Math.Clamp(noise, min, max), deltaTime);
|
||||
#if CLIENT
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos,
|
||||
-currForce / 5.0f + new Vector2(Rand.Range(-100.0f, 100.0f), Rand.Range(-50f, 50f)),
|
||||
0.0f, item.CurrentHull);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAITargets(float increaseSpeed, float deltaTime)
|
||||
{
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
item.AiTarget.IncreaseSoundRange(deltaTime, increaseSpeed);
|
||||
if (item.CurrentHull != null && item.CurrentHull.AiTarget != null)
|
||||
{
|
||||
// It's possible that some othe item increases the hull's soundrange more than the engine.
|
||||
item.CurrentHull.AiTarget.SoundRange = Math.Max(item.CurrentHull.AiTarget.SoundRange, item.AiTarget.SoundRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePropellerDamage(float deltaTime)
|
||||
{
|
||||
damageTimer += deltaTime;
|
||||
if (damageTimer < 0.5f) return;
|
||||
damageTimer = 0.1f;
|
||||
|
||||
if (propellerDamage == null) return;
|
||||
Vector2 propellerWorldPos = item.WorldPosition + PropellerPos;
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
if (character.Submarine != null || !character.Enabled || character.Removed) continue;
|
||||
|
||||
float dist = Vector2.DistanceSquared(character.WorldPosition, propellerWorldPos);
|
||||
if (dist > propellerDamage.DamageRange * propellerDamage.DamageRange) continue;
|
||||
|
||||
character.LastDamageSource = item;
|
||||
propellerDamage.DoDamage(null, character, propellerWorldPos, 1.0f, true);
|
||||
}
|
||||
}
|
||||
|
||||
partial void UpdateAnimation(float deltaTime);
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
{
|
||||
base.UpdateBroken(deltaTime, cam);
|
||||
force = MathHelper.Lerp(force, 0.0f, 0.1f);
|
||||
}
|
||||
|
||||
public override void FlipX(bool relativeToSub)
|
||||
{
|
||||
PropellerPos = new Vector2(-PropellerPos.X, PropellerPos.Y);
|
||||
}
|
||||
|
||||
public override void FlipY(bool relativeToSub)
|
||||
{
|
||||
PropellerPos = new Vector2(PropellerPos.X, -PropellerPos.Y);
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
|
||||
{
|
||||
base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength);
|
||||
|
||||
if (connection.Name == "set_force")
|
||||
{
|
||||
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float tempForce))
|
||||
{
|
||||
controlLockTimer = 0.1f;
|
||||
targetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
{
|
||||
Vector2 prevPropellerPos = PropellerPos;
|
||||
//undo flipping before saving
|
||||
if (item.FlippedX) { PropellerPos = new Vector2(-PropellerPos.X, PropellerPos.Y); }
|
||||
if (item.FlippedY) { PropellerPos = new Vector2(PropellerPos.X, -PropellerPos.Y); }
|
||||
XElement element = base.Save(parentElement);
|
||||
PropellerPos = prevPropellerPos;
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
|
||||
partial class Fabricator : Powered, IServerSerializable, IClientSerializable
|
||||
{
|
||||
private readonly List<FabricationRecipe> fabricationRecipes = new List<FabricationRecipe>();
|
||||
|
||||
private FabricationRecipe fabricatedItem;
|
||||
private float timeUntilReady;
|
||||
private float requiredTime;
|
||||
|
||||
private bool hasPower;
|
||||
|
||||
private Character user;
|
||||
|
||||
private ItemContainer inputContainer, outputContainer;
|
||||
|
||||
public ItemContainer InputContainer
|
||||
{
|
||||
get { return inputContainer; }
|
||||
}
|
||||
|
||||
public ItemContainer OutputContainer
|
||||
{
|
||||
get { return outputContainer; }
|
||||
}
|
||||
|
||||
private float progressState;
|
||||
|
||||
public Fabricator(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
|
||||
{
|
||||
foreach (FabricationRecipe recipe in itemPrefab.FabricationRecipes)
|
||||
{
|
||||
if (recipe.SuitableFabricatorIdentifiers.Length > 0)
|
||||
{
|
||||
if (!recipe.SuitableFabricatorIdentifiers.Any(i => item.prefab.Identifier == i || item.HasTag(i)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
fabricationRecipes.Add(recipe);
|
||||
}
|
||||
}
|
||||
|
||||
InitProjSpecific();
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
var containers = item.GetComponents<ItemContainer>().ToList();
|
||||
if (containers.Count < 2)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item \"" + item.Name + "\": Fabricators must have two ItemContainer components!");
|
||||
return;
|
||||
}
|
||||
|
||||
inputContainer = containers[0];
|
||||
outputContainer = containers[1];
|
||||
|
||||
foreach (var recipe in fabricationRecipes)
|
||||
{
|
||||
int ingredientCount = recipe.RequiredItems.Sum(it => it.Amount);
|
||||
if (ingredientCount > inputContainer.Capacity)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in item \"" + item.Name + "\": There's not enough room in the input inventory for the ingredients of \"" + recipe.TargetItem.Name + "\"!");
|
||||
}
|
||||
}
|
||||
|
||||
OnItemLoadedProjSpecific();
|
||||
}
|
||||
|
||||
partial void OnItemLoadedProjSpecific();
|
||||
|
||||
|
||||
partial void InitProjSpecific();
|
||||
|
||||
public override bool Select(Character character)
|
||||
{
|
||||
SelectProjSpecific(character);
|
||||
return base.Select(character);
|
||||
}
|
||||
|
||||
partial void SelectProjSpecific(Character character);
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
return (picker != null);
|
||||
}
|
||||
|
||||
public void RemoveFabricationRecipes(List<string> allowedIdentifiers)
|
||||
{
|
||||
for (int i = 0; i < fabricationRecipes.Count; i++)
|
||||
{
|
||||
if (!allowedIdentifiers.Contains(fabricationRecipes[i].TargetItem.Identifier))
|
||||
{
|
||||
fabricationRecipes.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
CreateRecipes();
|
||||
}
|
||||
|
||||
partial void CreateRecipes();
|
||||
|
||||
private void StartFabricating(FabricationRecipe selectedItem, Character user)
|
||||
{
|
||||
if (selectedItem == null) return;
|
||||
if (!outputContainer.Inventory.IsEmpty()) return;
|
||||
|
||||
#if CLIENT
|
||||
itemList.Enabled = false;
|
||||
activateButton.Text = TextManager.Get("FabricatorCancel");
|
||||
#endif
|
||||
|
||||
IsActive = true;
|
||||
this.user = user;
|
||||
fabricatedItem = selectedItem;
|
||||
MoveIngredientsToInputContainer(selectedItem);
|
||||
|
||||
requiredTime = GetRequiredTime(fabricatedItem, user);
|
||||
timeUntilReady = requiredTime;
|
||||
|
||||
inputContainer.Inventory.Locked = true;
|
||||
outputContainer.Inventory.Locked = true;
|
||||
|
||||
currPowerConsumption = powerConsumption;
|
||||
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
#if SERVER
|
||||
if (user != null)
|
||||
{
|
||||
GameServer.Log(user.LogName + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction);
|
||||
}
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
private void CancelFabricating(Character user = null)
|
||||
{
|
||||
if (fabricatedItem == null) { return; }
|
||||
|
||||
IsActive = false;
|
||||
fabricatedItem = null;
|
||||
this.user = null;
|
||||
|
||||
currPowerConsumption = 0.0f;
|
||||
|
||||
#if CLIENT
|
||||
itemList.Enabled = true;
|
||||
if (activateButton != null)
|
||||
{
|
||||
activateButton.Text = TextManager.Get("FabricatorCreate");
|
||||
}
|
||||
#endif
|
||||
progressState = 0.0f;
|
||||
|
||||
timeUntilReady = 0.0f;
|
||||
|
||||
inputContainer.Inventory.Locked = false;
|
||||
outputContainer.Inventory.Locked = false;
|
||||
|
||||
#if SERVER
|
||||
if (user != null)
|
||||
{
|
||||
GameServer.Log(user.LogName + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction);
|
||||
}
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (fabricatedItem == null || !CanBeFabricated(fabricatedItem))
|
||||
{
|
||||
CancelFabricating();
|
||||
return;
|
||||
}
|
||||
|
||||
progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime;
|
||||
|
||||
hasPower = Voltage >= MinVoltage;
|
||||
if (!hasPower) { return; }
|
||||
|
||||
var repairable = item.GetComponent<Repairable>();
|
||||
if (repairable != null)
|
||||
{
|
||||
repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f;
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
|
||||
if (powerConsumption <= 0) { Voltage = 1.0f; }
|
||||
|
||||
timeUntilReady -= deltaTime * Math.Min(Voltage, 1.0f);
|
||||
|
||||
if (timeUntilReady > 0.0f) { return; }
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
var availableIngredients = GetAvailableIngredients();
|
||||
foreach (FabricationRecipe.RequiredItem ingredient in fabricatedItem.RequiredItems)
|
||||
{
|
||||
for (int i = 0; i < ingredient.Amount; i++)
|
||||
{
|
||||
var availableItem = availableIngredients.FirstOrDefault(it => it != null && it.Prefab == ingredient.ItemPrefab && it.Condition >= ingredient.ItemPrefab.Health * ingredient.MinCondition);
|
||||
if (availableItem == null) { continue; }
|
||||
|
||||
//Item4 = use condition bool
|
||||
if (ingredient.UseCondition && availableItem.Condition - ingredient.ItemPrefab.Health * ingredient.MinCondition > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0
|
||||
{
|
||||
availableItem.Condition -= ingredient.ItemPrefab.Health * ingredient.MinCondition;
|
||||
continue;
|
||||
}
|
||||
availableIngredients.Remove(availableItem);
|
||||
Entity.Spawner.AddToRemoveQueue(availableItem);
|
||||
inputContainer.Inventory.RemoveItem(availableItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (outputContainer.Inventory.Items.All(i => i != null))
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition);
|
||||
}
|
||||
else
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition);
|
||||
}
|
||||
|
||||
if (user != null && !user.Removed)
|
||||
{
|
||||
foreach (Skill skill in fabricatedItem.RequiredSkills)
|
||||
{
|
||||
float userSkill = user.GetSkillLevel(skill.Identifier);
|
||||
user.Info.IncreaseSkillLevel(
|
||||
skill.Identifier,
|
||||
skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f),
|
||||
user.WorldPosition + Vector2.UnitY * 150.0f);
|
||||
}
|
||||
}
|
||||
|
||||
CancelFabricating();
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanBeFabricated(FabricationRecipe fabricableItem)
|
||||
{
|
||||
if (fabricableItem == null) { return false; }
|
||||
List<Item> availableIngredients = GetAvailableIngredients();
|
||||
return CanBeFabricated(fabricableItem, availableIngredients);
|
||||
}
|
||||
|
||||
private bool CanBeFabricated(FabricationRecipe fabricableItem, IEnumerable<Item> availableIngredients)
|
||||
{
|
||||
if (fabricableItem == null) { return false; }
|
||||
foreach (FabricationRecipe.RequiredItem requiredItem in fabricableItem.RequiredItems)
|
||||
{
|
||||
if (availableIngredients.Count(it => IsItemValidIngredient(it, requiredItem)) < requiredItem.Amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private float GetRequiredTime(FabricationRecipe fabricableItem, Character user)
|
||||
{
|
||||
float degreeOfSuccess = DegreeOfSuccess(user, fabricableItem.RequiredSkills);
|
||||
|
||||
float t = degreeOfSuccess < 0.5f ? degreeOfSuccess * degreeOfSuccess : degreeOfSuccess * 2;
|
||||
|
||||
//fabricating takes 100 times longer if degree of success is close to 0
|
||||
//characters with a higher skill than required can fabricate up to 100% faster
|
||||
return fabricableItem.RequiredTime / MathHelper.Clamp(t, 0.01f, 2.0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all items available in the input container and linked containers
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private List<Item> GetAvailableIngredients()
|
||||
{
|
||||
List<Item> availableIngredients = new List<Item>();
|
||||
availableIngredients.AddRange(inputContainer.Inventory.Items.Where(it => it != null));
|
||||
foreach (MapEntity linkedTo in item.linkedTo)
|
||||
{
|
||||
if (linkedTo is Item linkedItem)
|
||||
{
|
||||
var itemContainer = linkedItem.GetComponent<ItemContainer>();
|
||||
if (itemContainer == null) { continue; }
|
||||
|
||||
var deconstructor = linkedItem.GetComponent<Deconstructor>();
|
||||
if (deconstructor != null)
|
||||
{
|
||||
itemContainer = deconstructor.OutputContainer;
|
||||
}
|
||||
|
||||
availableIngredients.AddRange(itemContainer.Inventory.Items.Where(it => it != null));
|
||||
}
|
||||
}
|
||||
#if CLIENT
|
||||
if (Character.Controlled?.Inventory != null)
|
||||
{
|
||||
availableIngredients.AddRange(Character.Controlled.Inventory.Items.Distinct().Where(it => it != null));
|
||||
}
|
||||
#else
|
||||
if (user?.Inventory != null)
|
||||
{
|
||||
availableIngredients.AddRange(user.Inventory.Items.Distinct().Where(it => it != null));
|
||||
}
|
||||
#endif
|
||||
|
||||
return availableIngredients;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move the items required for fabrication into the input container.
|
||||
/// The method assumes that all the required ingredients are available either in the input container or linked containers.
|
||||
/// </summary>
|
||||
private void MoveIngredientsToInputContainer(FabricationRecipe targetItem)
|
||||
{
|
||||
//required ingredients that are already present in the input container
|
||||
List<Item> usedItems = new List<Item>();
|
||||
|
||||
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
|
||||
|
||||
var availableIngredients = GetAvailableIngredients();
|
||||
foreach (var requiredItem in targetItem.RequiredItems)
|
||||
{
|
||||
for (int i = 0; i < requiredItem.Amount; i++)
|
||||
{
|
||||
var matchingItem = availableIngredients.Find(it => !usedItems.Contains(it) && IsItemValidIngredient(it, requiredItem));
|
||||
if (matchingItem == null) { continue; }
|
||||
|
||||
availableIngredients.Remove(matchingItem);
|
||||
|
||||
if (matchingItem.ParentInventory == inputContainer.Inventory)
|
||||
{
|
||||
//already in input container, all good
|
||||
usedItems.Add(matchingItem);
|
||||
}
|
||||
else //in another inventory, we need to move the item
|
||||
{
|
||||
if (inputContainer.Inventory.Items.All(it => it != null))
|
||||
{
|
||||
var unneededItem = inputContainer.Inventory.Items.FirstOrDefault(it => !usedItems.Contains(it));
|
||||
unneededItem?.Drop(null, createNetworkEvent: !isClient);
|
||||
}
|
||||
inputContainer.Inventory.TryPutItem(matchingItem, user: null, createNetworkEvent: !isClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsItemValidIngredient(Item item, FabricationRecipe.RequiredItem requiredItem)
|
||||
{
|
||||
return
|
||||
item != null &&
|
||||
item.prefab == requiredItem.ItemPrefab &&
|
||||
item.Condition / item.Prefab.Health >= requiredItem.MinCondition;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class MiniMap : Powered
|
||||
{
|
||||
class HullData
|
||||
{
|
||||
public float? Oxygen;
|
||||
public float? Water;
|
||||
|
||||
public bool Distort;
|
||||
public float DistortionTimer;
|
||||
|
||||
public List<Hull> LinkedHulls = new List<Hull>();
|
||||
}
|
||||
|
||||
private DateTime resetDataTime;
|
||||
|
||||
private bool hasPower;
|
||||
|
||||
private readonly Dictionary<Hull, HullData> hullDatas;
|
||||
|
||||
[Editable, Serialize(false, true, description: "Does the machine require inputs from water detectors in order to show the water levels inside rooms.")]
|
||||
public bool RequireWaterDetectors
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, true, description: "Does the machine require inputs from oxygen detectors in order to show the oxygen levels inside rooms.")]
|
||||
public bool RequireOxygenDetectors
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, true, description: "Should damaged walls be displayed by the machine.")]
|
||||
public bool ShowHullIntegrity
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public MiniMap(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
hullDatas = new Dictionary<Hull, HullData>();
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
//periodically reset all hull data
|
||||
//(so that outdated hull info won't be shown if detectors stop sending signals)
|
||||
if (DateTime.Now > resetDataTime)
|
||||
{
|
||||
foreach (HullData hullData in hullDatas.Values)
|
||||
{
|
||||
if (!hullData.Distort)
|
||||
{
|
||||
hullData.Oxygen = null;
|
||||
hullData.Water = null;
|
||||
}
|
||||
}
|
||||
resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1);
|
||||
}
|
||||
|
||||
currPowerConsumption = powerConsumption;
|
||||
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
hasPower = Voltage > MinVoltage;
|
||||
if (hasPower)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
return picker != null;
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1.0f)
|
||||
{
|
||||
if (source == null || source.CurrentHull == null) { return; }
|
||||
|
||||
Hull sourceHull = source.CurrentHull;
|
||||
if (!hullDatas.TryGetValue(sourceHull, out HullData hullData))
|
||||
{
|
||||
hullData = new HullData();
|
||||
hullDatas.Add(sourceHull, hullData);
|
||||
}
|
||||
|
||||
if (hullData.Distort) return;
|
||||
|
||||
switch (connection.Name)
|
||||
{
|
||||
case "water_data_in":
|
||||
//cheating a bit because water detectors don't actually send the water level
|
||||
if (source.GetComponent<WaterDetector>() == null)
|
||||
{
|
||||
hullData.Water = Rand.Range(0.0f, 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
hullData.Water = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f);
|
||||
}
|
||||
break;
|
||||
case "oxygen_data_in":
|
||||
float oxy;
|
||||
|
||||
if (!float.TryParse(signal, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out oxy))
|
||||
{
|
||||
oxy = Rand.Range(0.0f, 100.0f);
|
||||
}
|
||||
|
||||
hullData.Oxygen = oxy;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class OxygenGenerator : Powered
|
||||
{
|
||||
private float powerDownTimer;
|
||||
|
||||
private float generatedAmount;
|
||||
|
||||
private List<Vent> ventList;
|
||||
|
||||
private float totalHullVolume;
|
||||
|
||||
public float CurrFlow
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(400.0f, true, description: "How much oxygen the machine generates when operating at full power.")]
|
||||
public float GeneratedAmount
|
||||
{
|
||||
get { return generatedAmount; }
|
||||
set { generatedAmount = MathHelper.Clamp(value, -10000.0f, 10000.0f); }
|
||||
}
|
||||
|
||||
public OxygenGenerator(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
UpdateOnActiveEffects(deltaTime);
|
||||
|
||||
CurrFlow = 0.0f;
|
||||
currPowerConsumption = powerConsumption;
|
||||
//consume more power when in a bad condition
|
||||
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
if (powerConsumption <= 0.0f)
|
||||
{
|
||||
Voltage = 1.0f;
|
||||
}
|
||||
|
||||
if (item.CurrentHull == null) return;
|
||||
|
||||
if (Voltage < MinVoltage)
|
||||
{
|
||||
powerDownTimer += deltaTime;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
powerDownTimer = 0.0f;
|
||||
}
|
||||
|
||||
CurrFlow = Math.Min(Voltage, 1.0f) * generatedAmount * 100.0f;
|
||||
|
||||
//less effective when in bad condition
|
||||
float conditionMult = item.Condition / item.MaxCondition;
|
||||
//100% condition = 100% oxygen
|
||||
//50% condition = 25% oxygen
|
||||
//20% condition = 4%
|
||||
CurrFlow *= conditionMult * conditionMult;
|
||||
|
||||
UpdateVents(CurrFlow);
|
||||
}
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
{
|
||||
base.UpdateBroken(deltaTime, cam);
|
||||
powerDownTimer += deltaTime;
|
||||
CurrFlow = 0.0f;
|
||||
}
|
||||
|
||||
private void GetVents()
|
||||
{
|
||||
ventList.Clear();
|
||||
|
||||
foreach (MapEntity entity in item.linkedTo)
|
||||
{
|
||||
Item linkedItem = entity as Item;
|
||||
if (linkedItem == null) continue;
|
||||
|
||||
Vent vent = linkedItem.GetComponent<Vent>();
|
||||
if (vent == null) continue;
|
||||
|
||||
ventList.Add(vent);
|
||||
if (linkedItem.CurrentHull != null) totalHullVolume += linkedItem.CurrentHull.Volume;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVents(float deltaOxygen)
|
||||
{
|
||||
if (ventList == null)
|
||||
{
|
||||
ventList = new List<Vent>();
|
||||
GetVents();
|
||||
}
|
||||
|
||||
if (!ventList.Any() || totalHullVolume <= 0.0f) return;
|
||||
|
||||
foreach (Vent v in ventList)
|
||||
{
|
||||
if (v.Item.CurrentHull == null) continue;
|
||||
|
||||
v.OxygenFlow = deltaOxygen * (v.Item.CurrentHull.Volume / totalHullVolume);
|
||||
v.IsActive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Pump : Powered, IServerSerializable, IClientSerializable
|
||||
{
|
||||
private float flowPercentage;
|
||||
private float maxFlow;
|
||||
|
||||
private float? targetLevel;
|
||||
|
||||
private float pumpSpeedLockTimer, isActiveLockTimer;
|
||||
|
||||
[Serialize(0.0f, true, description: "How fast the item is currently pumping water (-100 = full speed out, 100 = full speed in). Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
|
||||
public float FlowPercentage
|
||||
{
|
||||
get { return flowPercentage; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(flowPercentage)) { return; }
|
||||
flowPercentage = MathHelper.Clamp(value, -100.0f, 100.0f);
|
||||
flowPercentage = MathUtils.Round(flowPercentage, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
[Editable, Serialize(80.0f, false, description: "How fast the item pumps water in/out when operating at 100%.")]
|
||||
public float MaxFlow
|
||||
{
|
||||
get { return maxFlow; }
|
||||
set { maxFlow = value; }
|
||||
}
|
||||
|
||||
private float currFlow;
|
||||
public float CurrFlow
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsActive) { return 0.0f; }
|
||||
return Math.Abs(currFlow);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsActive
|
||||
{
|
||||
get => base.IsActive;
|
||||
set
|
||||
{
|
||||
base.IsActive = value;
|
||||
if (!IsActive)
|
||||
{
|
||||
powerConsumption = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasPower => IsActive && Voltage >= MinVoltage;
|
||||
|
||||
public Pump(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
currFlow = 0.0f;
|
||||
|
||||
if (targetLevel != null)
|
||||
{
|
||||
pumpSpeedLockTimer -= deltaTime;
|
||||
float hullPercentage = 0.0f;
|
||||
if (item.CurrentHull != null) { hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; }
|
||||
FlowPercentage = ((float)targetLevel - hullPercentage) * 10.0f;
|
||||
|
||||
if (pumpSpeedLockTimer <= 0.0f)
|
||||
{
|
||||
targetLevel = null;
|
||||
}
|
||||
}
|
||||
|
||||
currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f);
|
||||
//pumps consume more power when in a bad condition
|
||||
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
if (!HasPower) { return; }
|
||||
|
||||
UpdateProjSpecific(deltaTime);
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
|
||||
if (item.CurrentHull == null) { return; }
|
||||
|
||||
float powerFactor = Math.Min(currPowerConsumption <= 0.0f ? 1.0f : Voltage, 1.0f);
|
||||
|
||||
currFlow = flowPercentage / 100.0f * maxFlow * powerFactor;
|
||||
//less effective when in a bad condition
|
||||
currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
item.CurrentHull.WaterVolume += currFlow;
|
||||
if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; }
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
|
||||
{
|
||||
if (connection.Name == "toggle")
|
||||
{
|
||||
IsActive = !IsActive;
|
||||
isActiveLockTimer = 0.1f;
|
||||
}
|
||||
else if (connection.Name == "set_active")
|
||||
{
|
||||
IsActive = signal != "0";
|
||||
isActiveLockTimer = 0.1f;
|
||||
}
|
||||
else if (connection.Name == "set_speed")
|
||||
{
|
||||
if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed))
|
||||
{
|
||||
flowPercentage = MathHelper.Clamp(tempSpeed, -100.0f, 100.0f);
|
||||
pumpSpeedLockTimer = 0.1f;
|
||||
}
|
||||
}
|
||||
else if (connection.Name == "set_targetlevel")
|
||||
{
|
||||
if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempTarget))
|
||||
{
|
||||
targetLevel = MathHelper.Clamp(tempTarget + 50.0f, 0.0f, 100.0f);
|
||||
pumpSpeedLockTimer = 0.1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
{
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null) { return false; }
|
||||
#endif
|
||||
|
||||
if (objective.Option.Equals("stoppumping", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
#if SERVER
|
||||
if (FlowPercentage > 0.0f)
|
||||
{
|
||||
item.CreateServerEvent(this);
|
||||
}
|
||||
#endif
|
||||
IsActive = false;
|
||||
FlowPercentage = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
#if SERVER
|
||||
if (!IsActive || FlowPercentage > -100.0f)
|
||||
{
|
||||
item.CreateServerEvent(this);
|
||||
}
|
||||
#endif
|
||||
IsActive = true;
|
||||
FlowPercentage = -100.0f;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Reactor : Powered, IServerSerializable, IClientSerializable
|
||||
{
|
||||
const float NetworkUpdateInterval = 0.5f;
|
||||
|
||||
//the rate at which the reactor is being run on (higher rate -> higher temperature)
|
||||
private float fissionRate;
|
||||
|
||||
//how much of the generated steam is used to spin the turbines and generate power
|
||||
private float turbineOutput;
|
||||
|
||||
private float temperature;
|
||||
|
||||
//is automatic temperature control on
|
||||
//(adjusts the fission rate and turbine output automatically to keep the
|
||||
//amount of power generated balanced with the load)
|
||||
private bool autoTemp;
|
||||
|
||||
//automatical adjustment to the power output when
|
||||
//turbine output and temperature are in the optimal range
|
||||
private float autoAdjustAmount;
|
||||
|
||||
private float fuelConsumptionRate;
|
||||
|
||||
private float meltDownTimer, meltDownDelay;
|
||||
private float fireTimer, fireDelay;
|
||||
|
||||
private float maxPowerOutput;
|
||||
|
||||
private Queue<float> loadQueue = new Queue<float>();
|
||||
private float load;
|
||||
|
||||
private bool unsentChanges;
|
||||
private float sendUpdateTimer;
|
||||
|
||||
private float degreeOfSuccess;
|
||||
|
||||
private Vector2 optimalTemperature, allowedTemperature;
|
||||
private Vector2 optimalFissionRate, allowedFissionRate;
|
||||
private Vector2 optimalTurbineOutput, allowedTurbineOutput;
|
||||
|
||||
private bool _powerOn;
|
||||
|
||||
[Serialize(defaultValue: false, isSaveable: true)]
|
||||
public bool PowerOn
|
||||
{
|
||||
get { return _powerOn; }
|
||||
set
|
||||
{
|
||||
_powerOn = value;
|
||||
#if CLIENT
|
||||
UpdateUIElementStates();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private Character lastAIUser;
|
||||
|
||||
private Character lastUser;
|
||||
private Character LastUser
|
||||
{
|
||||
get { return lastUser; }
|
||||
set
|
||||
{
|
||||
if (lastUser == value) return;
|
||||
lastUser = value;
|
||||
degreeOfSuccess = lastUser == null ? 0.0f : DegreeOfSuccess(lastUser);
|
||||
}
|
||||
}
|
||||
|
||||
[Editable(0.0f, float.MaxValue), Serialize(10000.0f, true, description: "How much power (kW) the reactor generates when operating at full capacity.")]
|
||||
public float MaxPowerOutput
|
||||
{
|
||||
get { return maxPowerOutput; }
|
||||
set
|
||||
{
|
||||
maxPowerOutput = Math.Max(0.0f, value);
|
||||
}
|
||||
}
|
||||
|
||||
[Editable(0.0f, float.MaxValue), Serialize(120.0f, true, description: "How long the temperature has to stay critical until a meltdown occurs.")]
|
||||
public float MeltdownDelay
|
||||
{
|
||||
get { return meltDownDelay; }
|
||||
set { meltDownDelay = Math.Max(value, 0.0f); }
|
||||
}
|
||||
|
||||
[Editable(0.0f, float.MaxValue), Serialize(30.0f, true, description: "How long the temperature has to stay critical until the reactor catches fire.")]
|
||||
public float FireDelay
|
||||
{
|
||||
get { return fireDelay; }
|
||||
set { fireDelay = Math.Max(value, 0.0f); }
|
||||
}
|
||||
|
||||
[Serialize(0.0f, true, description: "Current temperature of the reactor (0% - 100%). Indended to be used by StatusEffect conditionals.")]
|
||||
public float Temperature
|
||||
{
|
||||
get { return temperature; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
temperature = MathHelper.Clamp(value, 0.0f, 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(0.0f, true, description: "Current fission rate of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
|
||||
public float FissionRate
|
||||
{
|
||||
get { return fissionRate; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
fissionRate = MathHelper.Clamp(value, 0.0f, 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(0.0f, true, description: "Current turbine output of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
|
||||
public float TurbineOutput
|
||||
{
|
||||
get { return turbineOutput; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
turbineOutput = MathHelper.Clamp(value, 0.0f, 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(0.2f, true, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f)]
|
||||
public float FuelConsumptionRate
|
||||
{
|
||||
get { return fuelConsumptionRate; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
fuelConsumptionRate = Math.Max(value, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(false, true, description: "Is the temperature currently critical. Intended to be used by StatusEffect conditionals (setting the value from XML has no effect).")]
|
||||
public bool TemperatureCritical
|
||||
{
|
||||
get { return temperature > allowedTemperature.Y; }
|
||||
set { /*do nothing*/ }
|
||||
}
|
||||
|
||||
private float correctTurbineOutput;
|
||||
|
||||
private float targetFissionRate;
|
||||
private float targetTurbineOutput;
|
||||
|
||||
[Serialize(false, true, description: "Is the automatic temperature control currently on. Indended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
|
||||
public bool AutoTemp
|
||||
{
|
||||
get { return autoTemp; }
|
||||
set
|
||||
{
|
||||
autoTemp = value;
|
||||
#if CLIENT
|
||||
UpdateUIElementStates();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private float prevAvailableFuel;
|
||||
public float AvailableFuel { get; set; }
|
||||
|
||||
public Reactor(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
#if SERVER
|
||||
if (GameMain.Server != null && nextServerLogWriteTime != null)
|
||||
{
|
||||
if (Timing.TotalTime >= (float)nextServerLogWriteTime)
|
||||
{
|
||||
GameServer.Log(lastUser.LogName + " adjusted reactor settings: " +
|
||||
"Temperature: " + (int)(temperature * 100.0f) +
|
||||
", Fission rate: " + (int)targetFissionRate +
|
||||
", Turbine output: " + (int)targetTurbineOutput +
|
||||
(autoTemp ? ", Autotemp ON" : ", Autotemp OFF"),
|
||||
ServerLog.MessageType.ItemInteraction);
|
||||
|
||||
nextServerLogWriteTime = null;
|
||||
lastServerLogWriteTime = (float)Timing.TotalTime;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
//if an AI character was using the item on the previous frame but not anymore, turn autotemp on
|
||||
// (= bots turn autotemp back on when leaving the reactor)
|
||||
if (lastAIUser != null)
|
||||
{
|
||||
if (lastAIUser.SelectedConstruction != item && lastAIUser.CanInteractWith(item))
|
||||
{
|
||||
AutoTemp = true;
|
||||
unsentChanges = true;
|
||||
lastAIUser = null;
|
||||
}
|
||||
}
|
||||
|
||||
prevAvailableFuel = AvailableFuel;
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
|
||||
//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
|
||||
correctTurbineOutput += MathHelper.Clamp((load / MaxPowerOutput * 100.0f) - correctTurbineOutput, -10.0f, 10.0f) * deltaTime;
|
||||
|
||||
//calculate tolerances of the meters based on the skills of the user
|
||||
//more skilled characters have larger "sweet spots", making it easier to keep the power output at a suitable level
|
||||
float tolerance = MathHelper.Lerp(2.5f, 10.0f, degreeOfSuccess);
|
||||
optimalTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
|
||||
tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess);
|
||||
allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
|
||||
|
||||
optimalTemperature = Vector2.Lerp(new Vector2(40.0f, 60.0f), new Vector2(30.0f, 70.0f), degreeOfSuccess);
|
||||
allowedTemperature = Vector2.Lerp(new Vector2(30.0f, 70.0f), new Vector2(10.0f, 90.0f), degreeOfSuccess);
|
||||
|
||||
optimalFissionRate = Vector2.Lerp(new Vector2(30, AvailableFuel - 20), new Vector2(20, AvailableFuel - 10), degreeOfSuccess);
|
||||
optimalFissionRate.X = Math.Min(optimalFissionRate.X, optimalFissionRate.Y - 10);
|
||||
allowedFissionRate = Vector2.Lerp(new Vector2(20, AvailableFuel), new Vector2(10, AvailableFuel), degreeOfSuccess);
|
||||
allowedFissionRate.X = Math.Min(allowedFissionRate.X, allowedFissionRate.Y - 10);
|
||||
|
||||
float heatAmount = GetGeneratedHeat(fissionRate);
|
||||
float temperatureDiff = (heatAmount - turbineOutput) - Temperature;
|
||||
Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff));
|
||||
//if (item.InWater && AvailableFuel < 100.0f) Temperature -= 12.0f * deltaTime;
|
||||
|
||||
FissionRate = MathHelper.Lerp(fissionRate, Math.Min(targetFissionRate, AvailableFuel), deltaTime);
|
||||
TurbineOutput = MathHelper.Lerp(turbineOutput, targetTurbineOutput, deltaTime);
|
||||
|
||||
float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f);
|
||||
currPowerConsumption = -MaxPowerOutput * Math.Min(turbineOutput / 100.0f, temperatureFactor);
|
||||
|
||||
//if the turbine output and coolant flow are the optimal range,
|
||||
//make the generated power slightly adjust according to the load
|
||||
// (-> the reactor can automatically handle small changes in load as long as the values are roughly correct)
|
||||
if (turbineOutput > optimalTurbineOutput.X && turbineOutput < optimalTurbineOutput.Y &&
|
||||
temperature > optimalTemperature.X && temperature < optimalTemperature.Y)
|
||||
{
|
||||
float maxAutoAdjust = maxPowerOutput * 0.1f;
|
||||
autoAdjustAmount = MathHelper.Lerp(
|
||||
autoAdjustAmount,
|
||||
MathHelper.Clamp(-load - currPowerConsumption, -maxAutoAdjust, maxAutoAdjust),
|
||||
deltaTime * 10.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
autoAdjustAmount = MathHelper.Lerp(autoAdjustAmount, 0.0f, deltaTime * 10.0f);
|
||||
}
|
||||
currPowerConsumption += autoAdjustAmount;
|
||||
|
||||
if (!PowerOn)
|
||||
{
|
||||
targetFissionRate = 0.0f;
|
||||
targetTurbineOutput = 0.0f;
|
||||
}
|
||||
else if (autoTemp)
|
||||
{
|
||||
UpdateAutoTemp(2.0f, deltaTime);
|
||||
}
|
||||
float currentLoad = 0.0f;
|
||||
List<Connection> connections = item.Connections;
|
||||
if (connections != null && connections.Count > 0)
|
||||
{
|
||||
foreach (Connection connection in connections)
|
||||
{
|
||||
if (!connection.IsPower) { continue; }
|
||||
foreach (Connection recipient in connection.Recipients)
|
||||
{
|
||||
if (!(recipient.Item is Item it)) { continue; }
|
||||
|
||||
PowerTransfer pt = it.GetComponent<PowerTransfer>();
|
||||
if (pt == null) { continue; }
|
||||
|
||||
//calculate how much external power there is in the grid
|
||||
//(power coming from somewhere else than this reactor, e.g. batteries)
|
||||
float externalPower = Math.Max(CurrPowerConsumption - pt.CurrPowerConsumption, 0) * 0.95f;
|
||||
//reduce the external power from the load to prevent overloading the grid
|
||||
currentLoad = Math.Max(currentLoad, pt.PowerLoad - externalPower);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadQueue.Enqueue(currentLoad);
|
||||
while (loadQueue.Count() > 60.0f)
|
||||
{
|
||||
load = loadQueue.Average();
|
||||
loadQueue.Dequeue();
|
||||
}
|
||||
|
||||
if (fissionRate > 0.0f)
|
||||
{
|
||||
foreach (Item item in item.ContainedItems)
|
||||
{
|
||||
if (!item.HasTag("reactorfuel")) continue;
|
||||
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
|
||||
}
|
||||
|
||||
if (item.CurrentHull != null)
|
||||
{
|
||||
var aiTarget = item.CurrentHull.AiTarget;
|
||||
float range = Math.Abs(currPowerConsumption) / MaxPowerOutput;
|
||||
float noise = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range);
|
||||
aiTarget.SoundRange = Math.Max(aiTarget.SoundRange, noise);
|
||||
}
|
||||
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
var aiTarget = item.AiTarget;
|
||||
float range = Math.Abs(currPowerConsumption) / MaxPowerOutput;
|
||||
aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range);
|
||||
}
|
||||
}
|
||||
|
||||
item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null);
|
||||
|
||||
UpdateFailures(deltaTime);
|
||||
#if CLIENT
|
||||
UpdateGraph(deltaTime);
|
||||
#endif
|
||||
AvailableFuel = 0.0f;
|
||||
|
||||
sendUpdateTimer = Math.Max(sendUpdateTimer - deltaTime, 0.0f);
|
||||
|
||||
if (unsentChanges && sendUpdateTimer <= 0.0f)
|
||||
{
|
||||
#if SERVER
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
item.CreateServerEvent(this);
|
||||
}
|
||||
#endif
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null)
|
||||
{
|
||||
item.CreateClientEvent(this);
|
||||
}
|
||||
#endif
|
||||
sendUpdateTimer = NetworkUpdateInterval;
|
||||
unsentChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
private float GetGeneratedHeat(float fissionRate)
|
||||
{
|
||||
return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do we need more fuel to generate enough power to match the current load.
|
||||
/// </summary>
|
||||
/// <param name="minimumOutputRatio">How low we allow the output/load ratio to go before loading more fuel.
|
||||
/// 1.0 = always load more fuel when maximum output is too low, 0.5 = load more if max output is 50% of the load</param>
|
||||
private bool NeedMoreFuel(float minimumOutputRatio, float minCondition = 0)
|
||||
{
|
||||
float remainingFuel = item.ContainedItems.Sum(i => i.Condition);
|
||||
if (remainingFuel <= minCondition && load > 0.0f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//fission rate is clamped to the amount of available fuel
|
||||
float maxFissionRate = Math.Min(prevAvailableFuel, 100.0f);
|
||||
float maxTurbineOutput = 100.0f;
|
||||
|
||||
//calculate the maximum output if the fission rate is cranked as high as it goes and turbine output is at max
|
||||
float theoreticalMaxHeat = GetGeneratedHeat(fissionRate: maxFissionRate);
|
||||
float temperatureFactor = Math.Min(theoreticalMaxHeat / 50.0f, 1.0f);
|
||||
float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * MaxPowerOutput;
|
||||
|
||||
//maximum output not enough, we need more fuel
|
||||
return theoreticalMaxOutput < load * minimumOutputRatio;
|
||||
}
|
||||
|
||||
private bool TooMuchFuel()
|
||||
{
|
||||
var containedItems = item.ContainedItems;
|
||||
if (containedItems != null && containedItems.Count() <= 1) { return false; }
|
||||
|
||||
//get the amount of heat we'd generate if the fission rate was at the low end of the optimal range
|
||||
float minimumHeat = GetGeneratedHeat(optimalFissionRate.X);
|
||||
|
||||
//if we need a very high turbine output to keep the engine from overheating, there's too much fuel
|
||||
return minimumHeat > Math.Min(correctTurbineOutput * 1.5f, 90);
|
||||
}
|
||||
|
||||
private void UpdateFailures(float deltaTime)
|
||||
{
|
||||
if (temperature > allowedTemperature.Y)
|
||||
{
|
||||
item.SendSignal(0, "1", "meltdown_warning", null);
|
||||
//faster meltdown if the item is in a bad condition
|
||||
meltDownTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
|
||||
|
||||
if (meltDownTimer > MeltdownDelay)
|
||||
{
|
||||
MeltDown();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
item.SendSignal(0, "0", "meltdown_warning", null);
|
||||
meltDownTimer = Math.Max(0.0f, meltDownTimer - deltaTime);
|
||||
}
|
||||
|
||||
if (temperature > optimalTemperature.Y)
|
||||
{
|
||||
float prevFireTimer = fireTimer;
|
||||
fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
|
||||
|
||||
|
||||
#if SERVER
|
||||
if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedConstruction == item)
|
||||
{
|
||||
GameMain.Server.KarmaManager.OnReactorOverHeating(blameOnBroken.Character, deltaTime);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (fireTimer >= FireDelay && prevFireTimer < fireDelay)
|
||||
{
|
||||
new FireSource(item.WorldPosition);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fireTimer = Math.Max(0.0f, fireTimer - deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAutoTemp(float speed, float deltaTime)
|
||||
{
|
||||
float desiredTurbineOutput = (optimalTurbineOutput.X + optimalTurbineOutput.Y) / 2.0f;
|
||||
targetTurbineOutput += MathHelper.Clamp(desiredTurbineOutput - targetTurbineOutput, -speed, speed) * deltaTime;
|
||||
targetTurbineOutput = MathHelper.Clamp(targetTurbineOutput, 0.0f, 100.0f);
|
||||
|
||||
float desiredFissionRate = (optimalFissionRate.X + optimalFissionRate.Y) / 2.0f;
|
||||
targetFissionRate += MathHelper.Clamp(desiredFissionRate - targetFissionRate, -speed, speed) * deltaTime;
|
||||
|
||||
if (temperature > (optimalTemperature.X + optimalTemperature.Y) / 2.0f)
|
||||
{
|
||||
targetFissionRate = Math.Min(targetFissionRate - speed * 2 * deltaTime, allowedFissionRate.Y);
|
||||
}
|
||||
else if (-currPowerConsumption < load)
|
||||
{
|
||||
targetFissionRate = Math.Min(targetFissionRate + speed * 2 * deltaTime, 100.0f);
|
||||
}
|
||||
targetFissionRate = MathHelper.Clamp(targetFissionRate, 0.0f, 100.0f);
|
||||
|
||||
//don't push the target too far from the current fission rate
|
||||
//otherwise we may "overshoot", cranking the target fission rate all the way up because it takes a while
|
||||
//for the actual fission rate and temperature to follow
|
||||
targetFissionRate = MathHelper.Clamp(targetFissionRate, FissionRate - 5, FissionRate + 5);
|
||||
}
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
{
|
||||
base.UpdateBroken(deltaTime, cam);
|
||||
|
||||
item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null);
|
||||
|
||||
currPowerConsumption = 0.0f;
|
||||
Temperature -= deltaTime * 1000.0f;
|
||||
targetFissionRate = Math.Max(targetFissionRate - deltaTime * 10.0f, 0.0f);
|
||||
targetTurbineOutput = Math.Max(targetTurbineOutput - deltaTime * 10.0f, 0.0f);
|
||||
#if CLIENT
|
||||
FissionRateScrollBar.BarScroll = 1.0f - FissionRate / 100.0f;
|
||||
TurbineOutputScrollBar.BarScroll = 1.0f - TurbineOutput / 100.0f;
|
||||
UpdateGraph(deltaTime);
|
||||
#endif
|
||||
}
|
||||
|
||||
private void MeltDown()
|
||||
{
|
||||
if (item.Condition <= 0.0f) { return; }
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
|
||||
item.Condition = 0.0f;
|
||||
fireTimer = 0.0f;
|
||||
meltDownTimer = 0.0f;
|
||||
|
||||
var containedItems = item.ContainedItems;
|
||||
if (containedItems != null)
|
||||
{
|
||||
foreach (Item containedItem in containedItems)
|
||||
{
|
||||
if (containedItem == null) continue;
|
||||
containedItem.Condition = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
GameServer.Log("Reactor meltdown!", ServerLog.MessageType.ItemInteraction);
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
GameMain.Server.KarmaManager.OnReactorMeltdown(blameOnBroken?.Character);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
return picker != null;
|
||||
}
|
||||
|
||||
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
|
||||
|
||||
IsActive = true;
|
||||
|
||||
float degreeOfSuccess = DegreeOfSuccess(character);
|
||||
float refuelLimit = 0.3f;
|
||||
//characters with insufficient skill levels don't refuel the reactor
|
||||
if (degreeOfSuccess > refuelLimit)
|
||||
{
|
||||
if (objective.SubObjectives.None())
|
||||
{
|
||||
if (!AIDecontainEmptyItems(character, objective, equip: false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (aiUpdateTimer > 0.0f)
|
||||
{
|
||||
aiUpdateTimer -= deltaTime;
|
||||
return false;
|
||||
}
|
||||
aiUpdateTimer = AIUpdateInterval;
|
||||
|
||||
// load more fuel if the current maximum output is only 50% of the current load
|
||||
// or if the fuel rod is (almost) deplenished
|
||||
float minCondition = fuelConsumptionRate * MathUtils.Pow((degreeOfSuccess - refuelLimit) * 2, 2);
|
||||
if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition))
|
||||
{
|
||||
var container = item.GetComponent<ItemContainer>();
|
||||
if (objective.SubObjectives.None())
|
||||
{
|
||||
int itemCount = item.ContainedItems.Count(i => i != null && container.ContainableItems.Any(ri => ri.MatchesItem(i))) + 1;
|
||||
AIContainItems<Reactor>(container, character, objective, itemCount, equip: false, removeEmpty: true);
|
||||
character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else if (TooMuchFuel())
|
||||
{
|
||||
var container = item.GetComponent<ItemContainer>();
|
||||
foreach (Item item in item.ContainedItems)
|
||||
{
|
||||
if (item != null && container.ContainableItems.Any(ri => ri.MatchesItem(item)))
|
||||
{
|
||||
if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots))
|
||||
{
|
||||
item.Drop(character);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (objective.Override)
|
||||
{
|
||||
if (lastUser != null && lastUser != character && lastUser != lastAIUser)
|
||||
{
|
||||
if (lastUser.SelectedConstruction == item)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogReactorTaken"), null, 0.0f, "reactortaken", 10.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LastUser = lastAIUser = character;
|
||||
|
||||
bool prevAutoTemp = autoTemp;
|
||||
bool prevPowerOn = _powerOn;
|
||||
float prevFissionRate = targetFissionRate;
|
||||
float prevTurbineOutput = targetTurbineOutput;
|
||||
|
||||
switch (objective.Option.ToLowerInvariant())
|
||||
{
|
||||
case "powerup":
|
||||
PowerOn = true;
|
||||
if (objective.Override || !autoTemp)
|
||||
{
|
||||
//characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually
|
||||
if (degreeOfSuccess < 0.5f)
|
||||
{
|
||||
AutoTemp = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
AutoTemp = false;
|
||||
UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f);
|
||||
}
|
||||
}
|
||||
#if CLIENT
|
||||
FissionRateScrollBar.BarScroll = FissionRate / 100.0f;
|
||||
TurbineOutputScrollBar.BarScroll = TurbineOutput / 100.0f;
|
||||
#endif
|
||||
break;
|
||||
case "shutdown":
|
||||
PowerOn = false;
|
||||
AutoTemp = false;
|
||||
targetFissionRate = 0.0f;
|
||||
targetTurbineOutput = 0.0f;
|
||||
break;
|
||||
}
|
||||
|
||||
if (autoTemp != prevAutoTemp ||
|
||||
prevPowerOn != _powerOn ||
|
||||
Math.Abs(prevFissionRate - targetFissionRate) > 1.0f ||
|
||||
Math.Abs(prevTurbineOutput - targetTurbineOutput) > 1.0f)
|
||||
{
|
||||
unsentChanges = true;
|
||||
}
|
||||
|
||||
aiUpdateTimer = AIUpdateInterval;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f)
|
||||
{
|
||||
switch (connection.Name)
|
||||
{
|
||||
case "shutdown":
|
||||
if (targetFissionRate > 0.0f || targetTurbineOutput > 0.0f)
|
||||
{
|
||||
PowerOn = false;
|
||||
AutoTemp = false;
|
||||
targetFissionRate = 0.0f;
|
||||
targetTurbineOutput = 0.0f;
|
||||
unsentChanges = true;
|
||||
}
|
||||
break;
|
||||
case "set_fissionrate":
|
||||
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate))
|
||||
{
|
||||
FissionRate = newFissionRate;
|
||||
unsentChanges = true;
|
||||
}
|
||||
break;
|
||||
case "set_turbineoutput":
|
||||
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput))
|
||||
{
|
||||
TurbineOutput = newTurbineOutput;
|
||||
unsentChanges = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Sonar : Powered, IServerSerializable, IClientSerializable
|
||||
{
|
||||
public enum Mode
|
||||
{
|
||||
Active,
|
||||
Passive
|
||||
};
|
||||
|
||||
public const float DefaultSonarRange = 10000.0f;
|
||||
|
||||
class ConnectedTransducer
|
||||
{
|
||||
public readonly SonarTransducer Transducer;
|
||||
public float SignalStrength;
|
||||
public float DisconnectTimer;
|
||||
|
||||
public ConnectedTransducer(SonarTransducer transducer, float signalStrength, float disconnectTimer)
|
||||
{
|
||||
Transducer = transducer;
|
||||
SignalStrength = signalStrength;
|
||||
DisconnectTimer = disconnectTimer;
|
||||
}
|
||||
}
|
||||
|
||||
private const float DirectionalPingSector = 30.0f;
|
||||
private static readonly float DirectionalPingDotProduct;
|
||||
|
||||
static Sonar()
|
||||
{
|
||||
DirectionalPingDotProduct = (float)Math.Cos(MathHelper.ToRadians(DirectionalPingSector) * 0.5f);
|
||||
}
|
||||
|
||||
private float range;
|
||||
|
||||
private const float PingFrequency = 0.5f;
|
||||
|
||||
private Mode currentMode = Mode.Passive;
|
||||
|
||||
private class ActivePing
|
||||
{
|
||||
public float State;
|
||||
public bool IsDirectional;
|
||||
public Vector2 Direction;
|
||||
public float PrevPingRadius;
|
||||
}
|
||||
// rotating list of currently active pings
|
||||
private ActivePing[] activePings = new ActivePing[8];
|
||||
// total number of currently active pings, range [0, activePings.Length[
|
||||
private int activePingsCount;
|
||||
// currently active ping index on the above list
|
||||
private int currentPingIndex = -1;
|
||||
|
||||
private const float MinZoom = 1.0f, MaxZoom = 4.0f;
|
||||
private float zoom = 1.0f;
|
||||
|
||||
private bool useDirectionalPing = false;
|
||||
private Vector2 pingDirection = new Vector2(1.0f, 0.0f);
|
||||
|
||||
private bool aiPingCheckPending;
|
||||
|
||||
//the float value is a timer used for disconnecting the transducer if no signal is received from it for 1 second
|
||||
private readonly List<ConnectedTransducer> connectedTransducers;
|
||||
|
||||
public IEnumerable<SonarTransducer> ConnectedTransducers
|
||||
{
|
||||
get { return connectedTransducers.Select(t => t.Transducer); }
|
||||
}
|
||||
|
||||
[Serialize(DefaultSonarRange, false, description: "The maximum range of the sonar.")]
|
||||
public float Range
|
||||
{
|
||||
get { return range; }
|
||||
set
|
||||
{
|
||||
range = MathHelper.Clamp(value, 0.0f, 100000.0f);
|
||||
if (item?.AiTarget != null && item.AiTarget.MaxSoundRange <= 0)
|
||||
{
|
||||
item.AiTarget.MaxSoundRange = range;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(false, false, description: "Should the sonar display the walls of the submarine it is inside.")]
|
||||
public bool DetectSubmarineWalls
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(false, false, description: "Does the sonar have to be connected to external transducers to work.")]
|
||||
public bool UseTransducers
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public float Zoom
|
||||
{
|
||||
get { return zoom; }
|
||||
}
|
||||
|
||||
public Mode CurrentMode
|
||||
{
|
||||
get => currentMode;
|
||||
set
|
||||
{
|
||||
bool changed = currentMode != value;
|
||||
|
||||
currentMode = value;
|
||||
if (value == Mode.Passive)
|
||||
{
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
item.AiTarget.SectorDegrees = 360.0f;
|
||||
}
|
||||
}
|
||||
#if CLIENT
|
||||
if (changed) { prevPassivePingRadius = float.MaxValue; }
|
||||
UpdateGUIElements();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public Sonar(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
connectedTransducers = new List<ConnectedTransducer>();
|
||||
IsActive = true;
|
||||
InitProjSpecific(element);
|
||||
CurrentMode = Mode.Passive;
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
currPowerConsumption = powerConsumption;
|
||||
|
||||
UpdateOnActiveEffects(deltaTime);
|
||||
|
||||
if (UseTransducers)
|
||||
{
|
||||
foreach (ConnectedTransducer transducer in connectedTransducers)
|
||||
{
|
||||
transducer.DisconnectTimer -= deltaTime;
|
||||
}
|
||||
connectedTransducers.RemoveAll(t => t.DisconnectTimer <= 0.0f);
|
||||
}
|
||||
|
||||
for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
|
||||
{
|
||||
activePings[pingIndex].State += deltaTime * PingFrequency;
|
||||
}
|
||||
|
||||
if (currentMode == Mode.Active)
|
||||
{
|
||||
if ((Voltage >= MinVoltage) &&
|
||||
(!UseTransducers || connectedTransducers.Count > 0))
|
||||
{
|
||||
if (currentPingIndex != -1)
|
||||
{
|
||||
var activePing = activePings[currentPingIndex];
|
||||
if (activePing.State > 1.0f)
|
||||
{
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom);
|
||||
item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range);
|
||||
item.AiTarget.SectorDegrees = activePing.IsDirectional ? DirectionalPingSector : 360.0f;
|
||||
item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y);
|
||||
}
|
||||
aiPingCheckPending = true;
|
||||
currentPingIndex = -1;
|
||||
}
|
||||
}
|
||||
if (currentPingIndex == -1 && activePingsCount < activePings.Length)
|
||||
{
|
||||
currentPingIndex = activePingsCount++;
|
||||
if (activePings[currentPingIndex] == null)
|
||||
{
|
||||
activePings[currentPingIndex] = new ActivePing();
|
||||
}
|
||||
activePings[currentPingIndex].IsDirectional = useDirectionalPing;
|
||||
activePings[currentPingIndex].Direction = pingDirection;
|
||||
activePings[currentPingIndex].State = 0.0f;
|
||||
activePings[currentPingIndex].PrevPingRadius = 0.0f;
|
||||
item.Use(deltaTime);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (item.AiTarget != null)
|
||||
{
|
||||
item.AiTarget.SectorDegrees = 360.0f;
|
||||
}
|
||||
aiPingCheckPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (var pingIndex = 0; pingIndex < activePingsCount;)
|
||||
{
|
||||
if (activePings[pingIndex].State > 1.0f)
|
||||
{
|
||||
var lastIndex = --activePingsCount;
|
||||
var oldActivePing = activePings[pingIndex];
|
||||
activePings[pingIndex] = activePings[lastIndex];
|
||||
activePings[lastIndex] = oldActivePing;
|
||||
if (currentPingIndex == lastIndex)
|
||||
{
|
||||
currentPingIndex = pingIndex;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
++pingIndex;
|
||||
}
|
||||
}
|
||||
|
||||
Voltage -= deltaTime;
|
||||
}
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
return currentPingIndex != -1;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, List<Character>> targetGroups = new Dictionary<string, List<Character>>();
|
||||
|
||||
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
{
|
||||
if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; }
|
||||
|
||||
foreach (List<Character> targetGroup in targetGroups.Values)
|
||||
{
|
||||
targetGroup.Clear();
|
||||
}
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.AnimController.CurrentHull != null || !c.Enabled) continue;
|
||||
if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) continue;
|
||||
if (Vector2.DistanceSquared(c.WorldPosition, item.WorldPosition) > range * range) continue;
|
||||
|
||||
string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition);
|
||||
if (!targetGroups.ContainsKey(directionName))
|
||||
{
|
||||
targetGroups.Add(directionName, new List<Character>());
|
||||
}
|
||||
targetGroups[directionName].Add(c);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, List<Character>> targetGroup in targetGroups)
|
||||
{
|
||||
if (!targetGroup.Value.Any()) { continue; }
|
||||
string dialogTag = "DialogSonarTarget";
|
||||
if (targetGroup.Value.Count > 1)
|
||||
{
|
||||
dialogTag = "DialogSonarTargetMultiple";
|
||||
}
|
||||
else if (targetGroup.Value[0].Mass > 100.0f)
|
||||
{
|
||||
dialogTag = "DialogSonarTargetLarge";
|
||||
}
|
||||
|
||||
character.Speak(TextManager.GetWithVariables(dialogTag, new string[2] { "[direction]", "[count]" },
|
||||
new string[2] { targetGroup.Key.ToString(), targetGroup.Value.Count.ToString() },
|
||||
new bool[2] { true, false }), null, 0, "sonartarget" + targetGroup.Value[0].ID, 60);
|
||||
|
||||
//prevent the character from reporting other targets in the group
|
||||
for (int i = 1; i < targetGroup.Value.Count; i++)
|
||||
{
|
||||
character.DisableLine("sonartarget" + targetGroup.Value[i].ID);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private string GetDirectionName(Vector2 dir)
|
||||
{
|
||||
float angle = MathUtils.WrapAngleTwoPi((float)-Math.Atan2(dir.Y, dir.X) + MathHelper.PiOver2);
|
||||
|
||||
int clockDir = (int)Math.Round((angle / MathHelper.TwoPi) * 12);
|
||||
if (clockDir == 0) clockDir = 12;
|
||||
|
||||
return TextManager.GetWithVariable("roomname.subdiroclock", "[dir]", clockDir.ToString());
|
||||
}
|
||||
|
||||
private Vector2 GetTransducerPos()
|
||||
{
|
||||
if (!UseTransducers || connectedTransducers.Count == 0)
|
||||
{
|
||||
//use the position of the sub if the item is static (no body) and inside a sub
|
||||
return item.Submarine != null && item.body == null ? item.Submarine.WorldPosition : item.WorldPosition;
|
||||
}
|
||||
|
||||
Vector2 transducerPosSum = Vector2.Zero;
|
||||
foreach (ConnectedTransducer transducer in connectedTransducers)
|
||||
{
|
||||
if (transducer.Transducer.Item.Submarine != null)
|
||||
{
|
||||
return transducer.Transducer.Item.Submarine.WorldPosition;
|
||||
}
|
||||
transducerPosSum += transducer.Transducer.Item.WorldPosition;
|
||||
}
|
||||
return transducerPosSum / connectedTransducers.Count;
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1.0f)
|
||||
{
|
||||
base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength);
|
||||
|
||||
if (connection.Name == "transducer_in")
|
||||
{
|
||||
var transducer = source.GetComponent<SonarTransducer>();
|
||||
if (transducer == null) return;
|
||||
|
||||
var connectedTransducer = connectedTransducers.Find(t => t.Transducer == transducer);
|
||||
if (connectedTransducer == null)
|
||||
{
|
||||
connectedTransducers.Add(new ConnectedTransducer(transducer, signalStrength, 1.0f));
|
||||
}
|
||||
else
|
||||
{
|
||||
connectedTransducer.SignalStrength = signalStrength;
|
||||
connectedTransducer.DisconnectTimer = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
|
||||
{
|
||||
bool isActive = msg.ReadBoolean();
|
||||
bool directionalPing = useDirectionalPing;
|
||||
float zoomT = zoom, pingDirectionT = 0.0f;
|
||||
if (isActive)
|
||||
{
|
||||
zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
|
||||
directionalPing = msg.ReadBoolean();
|
||||
if (directionalPing)
|
||||
{
|
||||
pingDirectionT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.CanClientAccess(c)) { return; }
|
||||
|
||||
CurrentMode = isActive ? Mode.Active : Mode.Passive;
|
||||
|
||||
if (isActive)
|
||||
{
|
||||
zoom = MathHelper.Lerp(MinZoom, MaxZoom, zoomT);
|
||||
useDirectionalPing = directionalPing;
|
||||
if (useDirectionalPing)
|
||||
{
|
||||
float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, pingDirectionT);
|
||||
pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle));
|
||||
}
|
||||
#if CLIENT
|
||||
zoomSlider.BarScroll = zoomT;
|
||||
directionalModeSwitch.Selected = useDirectionalPing;
|
||||
#endif
|
||||
}
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
|
||||
{
|
||||
msg.Write(currentMode == Mode.Active);
|
||||
if (currentMode == Mode.Active)
|
||||
{
|
||||
msg.WriteRangedSingle(zoom, MinZoom, MaxZoom, 8);
|
||||
msg.Write(useDirectionalPing);
|
||||
if (useDirectionalPing)
|
||||
{
|
||||
float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection));
|
||||
msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class SonarTransducer : Powered
|
||||
{
|
||||
const float SendSignalInterval = 0.5f;
|
||||
|
||||
private float sendSignalTimer;
|
||||
|
||||
public SonarTransducer(Item item, XElement element) : base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
UpdateOnActiveEffects(deltaTime);
|
||||
|
||||
CurrPowerConsumption = powerConsumption;
|
||||
|
||||
if (Voltage >= MinVoltage)
|
||||
{
|
||||
sendSignalTimer += deltaTime;
|
||||
if (sendSignalTimer > SendSignalInterval)
|
||||
{
|
||||
item.SendSignal(0, "0101101101101011010", "data_out", sender: null);
|
||||
sendSignalTimer = SendSignalInterval;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Voronoi2;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Steering : Powered, IServerSerializable, IClientSerializable
|
||||
{
|
||||
private const float AutopilotRayCastInterval = 0.5f;
|
||||
private const float RecalculatePathInterval = 5.0f;
|
||||
|
||||
private const float AutopilotMinDistToPathNode = 30.0f;
|
||||
|
||||
private const float AutoPilotSteeringLerp = 0.1f;
|
||||
|
||||
private const float AutoPilotMaxSpeed = 0.5f;
|
||||
private const float AIPilotMaxSpeed = 1.0f;
|
||||
|
||||
private Vector2 currVelocity;
|
||||
private Vector2 targetVelocity;
|
||||
|
||||
private Vector2 steeringInput;
|
||||
|
||||
private bool autoPilot;
|
||||
|
||||
private Vector2? posToMaintain;
|
||||
|
||||
private SteeringPath steeringPath;
|
||||
|
||||
private PathFinder pathFinder;
|
||||
|
||||
private float networkUpdateTimer;
|
||||
private bool unsentChanges;
|
||||
|
||||
private float autopilotRayCastTimer;
|
||||
private float autopilotRecalculatePathTimer;
|
||||
|
||||
private Vector2 avoidStrength;
|
||||
|
||||
private float neutralBallastLevel;
|
||||
|
||||
private float steeringAdjustSpeed = 1.0f;
|
||||
|
||||
private Character user;
|
||||
|
||||
private Sonar sonar;
|
||||
|
||||
private Submarine controlledSub;
|
||||
|
||||
public bool AutoPilot
|
||||
{
|
||||
get { return autoPilot; }
|
||||
set
|
||||
{
|
||||
if (value == autoPilot) { return; }
|
||||
autoPilot = value;
|
||||
#if CLIENT
|
||||
UpdateGUIElements();
|
||||
#endif
|
||||
if (autoPilot)
|
||||
{
|
||||
if (pathFinder == null)
|
||||
{
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
}
|
||||
MaintainPos = true;
|
||||
if (posToMaintain == null)
|
||||
{
|
||||
posToMaintain = controlledSub != null ?
|
||||
controlledSub.WorldPosition :
|
||||
item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PosToMaintain = null;
|
||||
MaintainPos = false;
|
||||
LevelEndSelected = false;
|
||||
LevelStartSelected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Editable(0.0f, 1.0f, decimals: 3),
|
||||
Serialize(0.5f, true, description: "How full the ballast tanks should be when the submarine is not being steered upwards/downwards."
|
||||
+ " Can be used to compensate if the ballast tanks are too large/small relative to the size of the submarine.")]
|
||||
public float NeutralBallastLevel
|
||||
{
|
||||
get { return neutralBallastLevel; }
|
||||
set
|
||||
{
|
||||
neutralBallastLevel = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(1000.0f, true, description: "How close the docking port has to be to another docking port for the docking mode to become active.")]
|
||||
public float DockingAssistThreshold
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Vector2 TargetVelocity
|
||||
{
|
||||
get { return targetVelocity;}
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
targetVelocity.X = MathHelper.Clamp(value.X, -100.0f, 100.0f);
|
||||
targetVelocity.Y = MathHelper.Clamp(value.Y, -100.0f, 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 SteeringInput
|
||||
{
|
||||
get { return steeringInput; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
steeringInput.X = MathHelper.Clamp(value.X, -100.0f, 100.0f);
|
||||
steeringInput.Y = MathHelper.Clamp(value.Y, -100.0f, 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public SteeringPath SteeringPath
|
||||
{
|
||||
get { return steeringPath; }
|
||||
}
|
||||
|
||||
public Vector2? PosToMaintain
|
||||
{
|
||||
get { return posToMaintain; }
|
||||
set { posToMaintain = value; }
|
||||
}
|
||||
|
||||
struct ObstacleDebugInfo
|
||||
{
|
||||
public Vector2 Point1;
|
||||
public Vector2 Point2;
|
||||
|
||||
public Vector2? Intersection;
|
||||
|
||||
public float Dot;
|
||||
|
||||
public Vector2 AvoidStrength;
|
||||
|
||||
public ObstacleDebugInfo(GraphEdge edge, Vector2? intersection, float dot, Vector2 avoidStrength, Vector2 translation)
|
||||
{
|
||||
Point1 = edge.Point1 + translation;
|
||||
Point2 = edge.Point2 + translation;
|
||||
Intersection = intersection;
|
||||
Dot = dot;
|
||||
AvoidStrength = avoidStrength;
|
||||
}
|
||||
}
|
||||
|
||||
//edge point 1, edge point 2, avoid strength
|
||||
private List<ObstacleDebugInfo> debugDrawObstacles = new List<ObstacleDebugInfo>();
|
||||
|
||||
#region Docking
|
||||
public List<DockingPort> DockingSources = new List<DockingPort>();
|
||||
public DockingPort ActiveDockingSource, DockingTarget;
|
||||
private bool searchedConnectedDockingPort;
|
||||
|
||||
private bool dockingModeEnabled;
|
||||
public bool DockingModeEnabled
|
||||
{
|
||||
get { return UseAutoDocking && dockingModeEnabled; }
|
||||
set { dockingModeEnabled = value; }
|
||||
}
|
||||
|
||||
public bool UseAutoDocking
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = true;
|
||||
|
||||
private void FindConnectedDockingPort()
|
||||
{
|
||||
searchedConnectedDockingPort = true;
|
||||
foreach (MapEntity linkedTo in item.linkedTo)
|
||||
{
|
||||
if (linkedTo is Item item)
|
||||
{
|
||||
var port = item.GetComponent<DockingPort>();
|
||||
if (port != null)
|
||||
{
|
||||
DockingSources.Add(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dockingConnection = item.Connections.FirstOrDefault(c => c.Name == "toggle_docking");
|
||||
if (dockingConnection != null)
|
||||
{
|
||||
var connectedPorts = item.GetConnectedComponentsRecursive<DockingPort>(dockingConnection);
|
||||
DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.IsOutpost));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public Steering(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
sonar = item.GetComponent<Sonar>();
|
||||
}
|
||||
|
||||
public override bool Select(Character character)
|
||||
{
|
||||
if (!CanBeSelected) return false;
|
||||
|
||||
user = character;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (!searchedConnectedDockingPort)
|
||||
{
|
||||
FindConnectedDockingPort();
|
||||
}
|
||||
networkUpdateTimer -= deltaTime;
|
||||
if (unsentChanges)
|
||||
{
|
||||
if (networkUpdateTimer <= 0.0f)
|
||||
{
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null)
|
||||
{
|
||||
item.CreateClientEvent(this);
|
||||
correctionTimer = CorrectionDelay;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
#if SERVER
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
item.CreateServerEvent(this);
|
||||
}
|
||||
#endif
|
||||
|
||||
networkUpdateTimer = 0.1f;
|
||||
unsentChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
controlledSub = item.Submarine;
|
||||
var sonar = item.GetComponent<Sonar>();
|
||||
if (sonar != null && sonar.UseTransducers)
|
||||
{
|
||||
controlledSub = sonar.ConnectedTransducers.Any() ? sonar.ConnectedTransducers.First().Item.Submarine : null;
|
||||
}
|
||||
|
||||
currPowerConsumption = powerConsumption;
|
||||
|
||||
if (Voltage < MinVoltage) { return; }
|
||||
|
||||
if (user != null && user.Removed)
|
||||
{
|
||||
user = null;
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
|
||||
|
||||
float userSkill = 0.0f;
|
||||
if (user != null && (user.SelectedConstruction == item || item.linkedTo.Contains(user.SelectedConstruction)))
|
||||
{
|
||||
userSkill = user.GetSkillLevel("helm") / 100.0f;
|
||||
}
|
||||
|
||||
if (AutoPilot)
|
||||
{
|
||||
UpdateAutoPilot(deltaTime);
|
||||
targetVelocity = targetVelocity.ClampLength(MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (user != null && user.Info != null && user.SelectedConstruction == item)
|
||||
{
|
||||
user.Info.IncreaseSkillLevel(
|
||||
"helm",
|
||||
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / Math.Max(userSkill, 1.0f) * deltaTime,
|
||||
user.WorldPosition + Vector2.UnitY * 150.0f);
|
||||
}
|
||||
|
||||
Vector2 velocityDiff = steeringInput - targetVelocity;
|
||||
if (velocityDiff != Vector2.Zero)
|
||||
{
|
||||
if (steeringAdjustSpeed >= 0.99f)
|
||||
{
|
||||
TargetVelocity = steeringInput;
|
||||
}
|
||||
else
|
||||
{
|
||||
float steeringChange = 1.0f / (1.0f - steeringAdjustSpeed);
|
||||
steeringChange *= steeringChange * 10.0f;
|
||||
|
||||
TargetVelocity += Vector2.Normalize(velocityDiff) *
|
||||
Math.Min(steeringChange * deltaTime, velocityDiff.Length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item.SendSignal(0, targetVelocity.X.ToString(CultureInfo.InvariantCulture), "velocity_x_out", null);
|
||||
|
||||
float targetLevel = -targetVelocity.Y;
|
||||
targetLevel += (neutralBallastLevel - 0.5f) * 100.0f;
|
||||
|
||||
item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", null);
|
||||
}
|
||||
|
||||
private void UpdateAutoPilot(float deltaTime)
|
||||
{
|
||||
if (controlledSub == null) { return; }
|
||||
if (posToMaintain != null)
|
||||
{
|
||||
Vector2 steeringVel = GetSteeringVelocity((Vector2)posToMaintain, 10.0f);
|
||||
TargetVelocity = Vector2.Lerp(TargetVelocity, steeringVel, AutoPilotSteeringLerp);
|
||||
return;
|
||||
}
|
||||
|
||||
autopilotRayCastTimer -= deltaTime;
|
||||
autopilotRecalculatePathTimer -= deltaTime;
|
||||
if (autopilotRecalculatePathTimer <= 0.0f)
|
||||
{
|
||||
//periodically recalculate the path in case the sub ends up to a position
|
||||
//where it can't keep traversing the initially calculated path
|
||||
UpdatePath();
|
||||
autopilotRecalculatePathTimer = RecalculatePathInterval;
|
||||
}
|
||||
|
||||
steeringPath.CheckProgress(ConvertUnits.ToSimUnits(controlledSub.WorldPosition), 10.0f);
|
||||
|
||||
if (autopilotRayCastTimer <= 0.0f && steeringPath.NextNode != null)
|
||||
{
|
||||
Vector2 diff = ConvertUnits.ToSimUnits(steeringPath.NextNode.Position - controlledSub.WorldPosition);
|
||||
|
||||
//if the node is close enough, check if it's visible
|
||||
float lengthSqr = diff.LengthSquared();
|
||||
if (lengthSqr > 0.001f && lengthSqr < AutopilotMinDistToPathNode * AutopilotMinDistToPathNode)
|
||||
{
|
||||
diff = Vector2.Normalize(diff);
|
||||
|
||||
//check if the next waypoint is visible from all corners of the sub
|
||||
//(i.e. if we can navigate directly towards it or if there's obstacles in the way)
|
||||
bool nextVisible = true;
|
||||
for (int x = -1; x < 2; x += 2)
|
||||
{
|
||||
for (int y = -1; y < 2; y += 2)
|
||||
{
|
||||
Vector2 cornerPos =
|
||||
new Vector2(controlledSub.Borders.Width * x, controlledSub.Borders.Height * y) / 2.0f;
|
||||
|
||||
cornerPos = ConvertUnits.ToSimUnits(cornerPos * 1.1f + controlledSub.WorldPosition);
|
||||
|
||||
float dist = Vector2.Distance(cornerPos, steeringPath.NextNode.SimPosition);
|
||||
|
||||
if (Submarine.PickBody(cornerPos, cornerPos + diff * dist, null, Physics.CollisionLevel) == null) { continue; }
|
||||
|
||||
nextVisible = false;
|
||||
x = 2;
|
||||
y = 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextVisible) steeringPath.SkipToNextNode();
|
||||
}
|
||||
|
||||
autopilotRayCastTimer = AutopilotRayCastInterval;
|
||||
}
|
||||
|
||||
Vector2 newVelocity = Vector2.Zero;
|
||||
if (steeringPath.CurrentNode != null)
|
||||
{
|
||||
newVelocity = GetSteeringVelocity(steeringPath.CurrentNode.WorldPosition, 2.0f);
|
||||
}
|
||||
|
||||
Vector2 avoidDist = new Vector2(
|
||||
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.X), controlledSub.Borders.Width * 0.75f),
|
||||
Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.Y), controlledSub.Borders.Height * 0.75f));
|
||||
|
||||
float avoidRadius = avoidDist.Length();
|
||||
|
||||
Vector2 newAvoidStrength = Vector2.Zero;
|
||||
|
||||
debugDrawObstacles.Clear();
|
||||
|
||||
//steer away from nearby walls
|
||||
var closeCells = Level.Loaded.GetCells(controlledSub.WorldPosition, 4);
|
||||
foreach (VoronoiCell cell in closeCells)
|
||||
{
|
||||
foreach (GraphEdge edge in cell.Edges)
|
||||
{
|
||||
if (MathUtils.GetLineIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection))
|
||||
{
|
||||
Vector2 diff = controlledSub.WorldPosition - intersection;
|
||||
|
||||
//far enough -> ignore
|
||||
if (Math.Abs(diff.X) > avoidDist.X && Math.Abs(diff.Y) > avoidDist.Y)
|
||||
{
|
||||
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, 0.0f, Vector2.Zero, Vector2.Zero));
|
||||
continue;
|
||||
}
|
||||
if (diff.LengthSquared() < 1.0f) diff = Vector2.UnitY;
|
||||
|
||||
Vector2 normalizedDiff = Vector2.Normalize(diff);
|
||||
float dot = controlledSub.Velocity == Vector2.Zero ?
|
||||
0.0f : Vector2.Dot(controlledSub.Velocity, -normalizedDiff);
|
||||
|
||||
//not heading towards the wall -> ignore
|
||||
if (dot < 1.0)
|
||||
{
|
||||
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot, Vector2.Zero, cell.Translation));
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector2 change = (normalizedDiff * Math.Max((avoidRadius - diff.Length()), 0.0f)) / avoidRadius;
|
||||
if (change.LengthSquared() < 0.001f) { continue; }
|
||||
newAvoidStrength += change * (dot - 1.0f);
|
||||
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot - 1.0f, change * (dot - 1.0f), cell.Translation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
avoidStrength = Vector2.Lerp(avoidStrength, newAvoidStrength, deltaTime * 10.0f);
|
||||
TargetVelocity = Vector2.Lerp(TargetVelocity, newVelocity + avoidStrength * 100.0f, AutoPilotSteeringLerp);
|
||||
|
||||
//steer away from other subs
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
{
|
||||
if (sub == controlledSub) continue;
|
||||
if (controlledSub.DockedTo.Contains(sub)) continue;
|
||||
|
||||
float thisSize = Math.Max(controlledSub.Borders.Width, controlledSub.Borders.Height);
|
||||
float otherSize = Math.Max(sub.Borders.Width, sub.Borders.Height);
|
||||
|
||||
Vector2 diff = controlledSub.WorldPosition - sub.WorldPosition;
|
||||
float dist = diff == Vector2.Zero ? 0.0f : diff.Length();
|
||||
|
||||
//far enough -> ignore
|
||||
if (dist > thisSize + otherSize) continue;
|
||||
|
||||
Vector2 dir = dist <= 0.0001f ? Vector2.UnitY : diff / dist;
|
||||
float dot = controlledSub.Velocity == Vector2.Zero ?
|
||||
0.0f : Vector2.Dot(Vector2.Normalize(controlledSub.Velocity), -dir);
|
||||
|
||||
//heading away -> ignore
|
||||
if (dot < 0.0f) continue;
|
||||
|
||||
targetVelocity += diff * 200.0f;
|
||||
}
|
||||
|
||||
//clamp velocity magnitude to 100.0f
|
||||
float velMagnitude = targetVelocity.Length();
|
||||
if (velMagnitude > 100.0f)
|
||||
{
|
||||
targetVelocity *= 100.0f / velMagnitude;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePath()
|
||||
{
|
||||
if (pathFinder == null) pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
|
||||
Vector2 target;
|
||||
if (LevelEndSelected)
|
||||
{
|
||||
target = ConvertUnits.ToSimUnits(Level.Loaded.EndPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
target = ConvertUnits.ToSimUnits(Level.Loaded.StartPosition);
|
||||
}
|
||||
steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(controlledSub == null ? item.WorldPosition : controlledSub.WorldPosition), target, errorMsgStr: "(Autopilot, target: " + target + ")");
|
||||
}
|
||||
|
||||
public void SetDestinationLevelStart()
|
||||
{
|
||||
AutoPilot = true;
|
||||
MaintainPos = false;
|
||||
posToMaintain = null;
|
||||
LevelEndSelected = false;
|
||||
if (!LevelStartSelected)
|
||||
{
|
||||
LevelStartSelected = true;
|
||||
UpdatePath();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDestinationLevelEnd()
|
||||
{
|
||||
AutoPilot = true;
|
||||
MaintainPos = false;
|
||||
posToMaintain = null;
|
||||
LevelStartSelected = false;
|
||||
if (!LevelEndSelected)
|
||||
{
|
||||
LevelEndSelected = true;
|
||||
UpdatePath();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get optimal velocity for moving towards a position
|
||||
/// </summary>
|
||||
/// <param name="worldPosition">Position to steer towards to</param>
|
||||
/// <param name="slowdownAmount">How heavily the sub slows down when approaching the target</param>
|
||||
/// <returns></returns>
|
||||
private Vector2 GetSteeringVelocity(Vector2 worldPosition, float slowdownAmount)
|
||||
{
|
||||
Vector2 futurePosition = ConvertUnits.ToDisplayUnits(controlledSub.Velocity) * slowdownAmount;
|
||||
Vector2 targetSpeed = ((worldPosition - controlledSub.WorldPosition) - futurePosition);
|
||||
|
||||
if (targetSpeed.LengthSquared() > 500.0f * 500.0f)
|
||||
{
|
||||
return Vector2.Normalize(targetSpeed) * 100.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
return targetSpeed / 5.0f;
|
||||
}
|
||||
}
|
||||
|
||||
private bool aiDockingToggled;
|
||||
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
||||
{
|
||||
if (objective.Override)
|
||||
{
|
||||
if (user != character && user != null && user.SelectedConstruction == item)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogSteeringTaken"), null, 0.0f, "steeringtaken", 10.0f);
|
||||
}
|
||||
}
|
||||
user = character;
|
||||
if (!AutoPilot)
|
||||
{
|
||||
unsentChanges = true;
|
||||
AutoPilot = true;
|
||||
}
|
||||
switch (objective.Option.ToLowerInvariant())
|
||||
{
|
||||
case "maintainposition":
|
||||
if (objective.Override)
|
||||
{
|
||||
if (!MaintainPos)
|
||||
{
|
||||
unsentChanges = true;
|
||||
MaintainPos = true;
|
||||
}
|
||||
if (!posToMaintain.HasValue)
|
||||
{
|
||||
unsentChanges = true;
|
||||
posToMaintain = controlledSub != null ?
|
||||
controlledSub.WorldPosition :
|
||||
item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "navigateback":
|
||||
if (!aiDockingToggled && DockingSources.Any(d => d.Docked))
|
||||
{
|
||||
item.SendSignal(0, "1", "toggle_docking", sender: null);
|
||||
}
|
||||
if (objective.Override)
|
||||
{
|
||||
if (MaintainPos || LevelEndSelected || !LevelStartSelected)
|
||||
{
|
||||
unsentChanges = true;
|
||||
}
|
||||
SetDestinationLevelStart();
|
||||
}
|
||||
break;
|
||||
case "navigatetodestination":
|
||||
if (!aiDockingToggled && DockingSources.Any(d => d.Docked))
|
||||
{
|
||||
item.SendSignal(0, "1", "toggle_docking", sender: null);
|
||||
}
|
||||
if (objective.Override)
|
||||
{
|
||||
if (MaintainPos || !LevelEndSelected || LevelStartSelected)
|
||||
{
|
||||
unsentChanges = true;
|
||||
}
|
||||
SetDestinationLevelEnd();
|
||||
}
|
||||
break;
|
||||
}
|
||||
sonar?.AIOperate(deltaTime, character, objective);
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
|
||||
{
|
||||
if (connection.Name == "velocity_in")
|
||||
{
|
||||
currVelocity = XMLExtensions.ParseVector2(signal, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power, signalStrength);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
class Vent : ItemComponent
|
||||
{
|
||||
private float oxygenFlow;
|
||||
|
||||
public float OxygenFlow
|
||||
{
|
||||
get { return oxygenFlow; }
|
||||
set { oxygenFlow = Math.Max(value, 0.0f); }
|
||||
}
|
||||
|
||||
public Vent (Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (item.CurrentHull == null) return;
|
||||
|
||||
if (item.InWater) return;
|
||||
|
||||
item.CurrentHull.Oxygen += oxygenFlow * deltaTime;
|
||||
OxygenFlow -= deltaTime * 1000.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user