Unstable 1.8.4.0

This commit is contained in:
Markus Isberg
2025-03-12 12:56:27 +00:00
parent a4c3e868e4
commit a4a3427e4e
627 changed files with 29860 additions and 10018 deletions
@@ -183,6 +183,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Does the Controller require power to function (= to send signals and move the camera focus to a connected item)?")]
public bool RequirePower
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "If true, other items can be used simultaneously.")]
public bool IsSecondaryItem
{
@@ -190,6 +197,13 @@ namespace Barotrauma.Items.Components
private set;
}
[Serialize(false, IsPropertySaveable.No, description: "If enabled, the user sticks to the position of this item even if the item moves.")]
public bool ForceUserToStayAttached
{
get;
set;
}
public Controller(Item item, ContentXElement element)
: base(item, element)
{
@@ -199,25 +213,44 @@ namespace Barotrauma.Items.Components
IsActive = true;
}
/// <summary>
/// Hack for allowing characters to interact with a loader to get inside a boarding pod.
/// Doing that simply by autointeracting with the contained pod is difficult, because interacting with the loader selects it
/// _after_ the Select method of the pod is called by the autointeract logic, and the character only goes inside the pod if it's the selected item.
/// </summary>
private bool forceSelectNextFrame;
private float userCanInteractCheckTimer;
private const float UserCanInteractCheckInterval = 1.0f;
public override void Update(float deltaTime, Camera cam)
{
this.cam = cam;
UserInCorrectPosition = false;
if (!ForceUserToStayAttached) { UserInCorrectPosition = false; }
string signal = IsToggle && State ? output : falseOutput;
if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal))
if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal) && !IsOutOfPower())
{
item.SendSignal(signal, "signal_out");
item.SendSignal(signal, "trigger_out");
}
if (forceSelectNextFrame && user != null)
{
user.SelectedItem = item;
}
forceSelectNextFrame = false;
userCanInteractCheckTimer -= deltaTime;
if (user == null
|| user.Removed
|| !user.IsAnySelectedItem(item)
|| item.ParentInventory != null
|| !user.CanInteractWith(item)
|| (item.ParentInventory != null && !IsAttachedUser(user))
|| (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater))
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater)
|| !CheckUserCanInteract())
{
if (user != null)
{
@@ -228,6 +261,17 @@ namespace Barotrauma.Items.Components
return;
}
if (ForceUserToStayAttached && Vector2.DistanceSquared(item.WorldPosition, user.WorldPosition) > 0.1f)
{
user.TeleportTo(item.WorldPosition);
user.AnimController.Collider.ResetDynamics();
foreach (var limb in user.AnimController.Limbs)
{
if (limb.Removed || limb.IsSevered) { continue; }
limb.body?.ResetDynamics();
}
}
user.AnimController.StartUsingItem();
if (userPos != Vector2.Zero)
@@ -330,6 +374,22 @@ namespace Barotrauma.Items.Components
}
}
private bool CheckUserCanInteract()
{
//optimization: CanInteractWith is relatively heavy (can involve visibility checks for example), let's not do it every frame
if (user != null)
{
if (userCanInteractCheckTimer <= 0.0f)
{
userCanInteractCheckTimer = UserCanInteractCheckInterval;
return user.CanInteractWith(item);
}
}
//we only do the actual check every UserCanInteractCheckInterval seconds
//can mean the component can stay selected for <1s after the user no longer has access to it
return true;
}
private double lastUsed;
public override bool Use(float deltaTime, Character activator = null)
@@ -344,6 +404,8 @@ namespace Barotrauma.Items.Components
return false;
}
if (IsOutOfPower()) { return false; }
if (IsToggle && (activator == null || lastUsed < Timing.TotalTime - 0.1))
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
@@ -380,6 +442,8 @@ namespace Barotrauma.Items.Components
return false;
}
if (IsOutOfPower()) { return false; }
focusTarget = GetFocusTarget();
if (focusTarget == null)
@@ -417,11 +481,20 @@ namespace Barotrauma.Items.Components
return true;
}
public bool IsOutOfPower()
{
if (!RequirePower) { return false; }
var powered = item.GetComponent<Powered>();
return powered == null || powered.Voltage < powered.MinVoltage;
}
public Item GetFocusTarget()
{
var positionOut = item.Connections?.Find(c => c.Name == "position_out");
if (positionOut == null) { return null; }
if (IsOutOfPower()) { return null; }
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut);
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
@@ -447,6 +520,7 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
if (IsOutOfPower()) { return false; }
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { return false; }
#endif
@@ -539,7 +613,16 @@ namespace Barotrauma.Items.Components
{
user = activator;
IsActive = true;
if (ForceUserToStayAttached && item.Container != null)
{
forceSelectNextFrame = true;
return false;
}
}
//allow the selection logic above to run when out of power, but allow sending signals
if (IsOutOfPower()) { return false; }
#if SERVER
item.CreateServerEvent(this);
#endif
@@ -550,6 +633,14 @@ namespace Barotrauma.Items.Components
return true;
}
/// <summary>
/// "Attached user" sticks to this item. Can be used for things such as clown crates and boarding pods.
/// </summary>
public bool IsAttachedUser(Character character)
{
return character != null && character == user && ForceUserToStayAttached;
}
public override void FlipX(bool relativeToSub)
{
if (dir != Direction.None)
@@ -14,8 +14,6 @@ namespace Barotrauma.Items.Components
private float progressTimer;
private float progressState;
private bool hasPower;
private Character user;
private float userDeconstructorSpeedMultiplier = 1.0f;
@@ -88,9 +86,8 @@ namespace Barotrauma.Items.Components
SetActive(false);
return;
}
hasPower = Voltage >= MinVoltage;
if (!hasPower) { return; }
if (!HasPower) { return; }
var repairable = item.GetComponent<Repairable>();
if (repairable != null)
@@ -243,16 +240,16 @@ namespace Barotrauma.Items.Components
if (targetItem == otherItem) { continue; }
if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r == otherItem.Prefab.Identifier))
{
var geneticMaterial1 = targetItem.GetComponent<GeneticMaterial>();
var geneticMaterial2 = otherItem.GetComponent<GeneticMaterial>();
if (geneticMaterial1 != null && geneticMaterial2 != null)
var targetGeneticMaterial = targetItem.GetComponent<GeneticMaterial>();
var otherGeneticMaterial = otherItem.GetComponent<GeneticMaterial>();
if (targetGeneticMaterial != null && otherGeneticMaterial != null)
{
var result = geneticMaterial1.Combine(geneticMaterial2, user);
var result = targetGeneticMaterial.Combine(otherGeneticMaterial, user, out Item itemToDestroy);
if (result == GeneticMaterial.CombineResult.Refined)
{
inputContainer.Inventory.RemoveItem(otherItem);
OutputContainer.Inventory.RemoveItem(otherItem);
Entity.Spawner.AddItemToRemoveQueue(otherItem);
Entity.Spawner.AddItemToRemoveQueue(itemToDestroy);
}
if (result != GeneticMaterial.CombineResult.None)
{
@@ -425,31 +422,39 @@ namespace Barotrauma.Items.Components
foreach (Item inputItem in items)
{
if (!inputItem.AllowDeconstruct) { continue; }
foreach (var deconstructItem in inputItem.Prefab.DeconstructItems)
{
// check for deconstructor compatibility (for example, 'geneticresearchstation' tag)
if (deconstructItem.RequiredDeconstructor.Length > 0)
{
if (!deconstructItem.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier == r)) { continue; }
if (deconstructItem.RequiredDeconstructor.None(requiredId => item.HasTag(requiredId) || item.Prefab.Identifier == requiredId)) { continue; }
}
// check for other required items in the same deconstructor (for example, 'geneticmaterial')
if (deconstructItem.RequiredOtherItem.Length > 0 && checkRequiredOtherItems)
{
if (!deconstructItem.RequiredOtherItem.Any(r => items.Any(it => it.HasTag(r) || it.Prefab.Identifier == r))) { continue; }
// no matching item with the required id, skip
if (deconstructItem.RequiredOtherItem.None(requiredId => items.Any(it => it.HasTag(requiredId) || it.Prefab.Identifier == requiredId))) { continue; }
bool validOtherItemFound = false;
foreach (Item otherInputItem in items)
{
if (otherInputItem == inputItem) { continue; }
if (!deconstructItem.RequiredOtherItem.Any(r => otherInputItem.HasTag(r) || otherInputItem.Prefab.Identifier == r)) { continue; }
if (deconstructItem.RequiredOtherItem.None(requiredId => otherInputItem.HasTag(requiredId) || otherInputItem.Prefab.Identifier == requiredId)) { continue; }
var geneticMaterial1 = inputItem.GetComponent<GeneticMaterial>();
var geneticMaterial2 = otherInputItem.GetComponent<GeneticMaterial>();
if (geneticMaterial1 != null && geneticMaterial2 != null)
// skip if genetic materials cannot be combined (or refined)
var geneticMaterial = inputItem.GetComponent<GeneticMaterial>();
var otherGeneticMaterial = otherInputItem.GetComponent<GeneticMaterial>();
if (geneticMaterial != null && otherGeneticMaterial != null)
{
if (!geneticMaterial1.CanBeCombinedWith(geneticMaterial2)) { continue; }
if (!geneticMaterial.CanBeCombinedWith(otherGeneticMaterial)) { continue; }
}
validOtherItemFound = true;
}
if (!validOtherItemFound) { continue; }
}
yield return (inputItem, deconstructItem);
}
}
@@ -126,7 +126,7 @@ namespace Barotrauma.Items.Components
}
else
{
hasPower = Voltage > MinVoltage;
hasPower = HasPower;
}
if (lastReceivedTargetForce.HasValue)
@@ -146,7 +146,7 @@ namespace Barotrauma.Items.Components
float forceMultiplier = 0.1f;
if (User != null)
{
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel(Tags.HelmSkill) / 100));
}
currForce *= item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
@@ -206,7 +206,7 @@ namespace Barotrauma.Items.Components
private void StartFabricating(FabricationRecipe selectedItem, Character user, bool addToServerLog = true)
{
if (selectedItem == null) { return; }
if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
if (!outputContainer.Inventory.CanProbablyBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
IsActive = true;
this.user = user;
@@ -319,7 +319,7 @@ namespace Barotrauma.Items.Components
}
else
{
hasPower = Voltage >= MinVoltage;
hasPower = HasPower;
if (!hasPower)
{
@@ -499,12 +499,13 @@ namespace Barotrauma.Items.Components
{
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier);
}
InvSlotType invSlot = fabricatedItem.MoveToSlot;
if (i < amountFittingContainer)
{
Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality,
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
onItemSpawned(spawnedItem, tempUser, invSlot);
spawnedItem.Quality = quality;
spawnedItem.StolenDuringRound = ingredientsStolen;
spawnedItem.AllowStealing = ingredientsAllowStealing;
@@ -517,7 +518,7 @@ namespace Barotrauma.Items.Components
Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality,
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
onItemSpawned(spawnedItem, tempUser, invSlot);
spawnedItem.Quality = quality;
spawnedItem.StolenDuringRound = ingredientsStolen;
spawnedItem.AllowStealing = ingredientsAllowStealing;
@@ -527,15 +528,28 @@ namespace Barotrauma.Items.Components
}
}
void onItemSpawned(Item spawnedItem, Character user)
void onItemSpawned(Item spawnedItem, Character user, InvSlotType slot)
{
if (user != null && user.TeamID != CharacterTeamType.None)
CharacterTeamType teamID = CharacterTeamType.None;
if (user != null)
{
teamID = user.TeamID;
}
else if (item.Submarine != null)
{
teamID = item.Submarine.TeamID;
}
if (teamID != CharacterTeamType.None)
{
foreach (WifiComponent wifiComponent in spawnedItem.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = user.TeamID;
wifiComponent.TeamID = teamID;
}
}
if (slot != InvSlotType.None)
{
user?.Inventory.TryPutItem(spawnedItem, user, slot.ToEnumerable());
}
OnItemFabricated?.Invoke(spawnedItem, user);
}
if (user?.Info != null && !user.Removed)
@@ -562,7 +576,6 @@ namespace Barotrauma.Items.Components
StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false);
}
}
}
/// <summary>
@@ -720,17 +733,24 @@ namespace Barotrauma.Items.Components
private readonly HashSet<Item> usedIngredients = new HashSet<Item>();
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
public bool MissingRequiredRecipe(FabricationRecipe fabricableItem, Character character)
{
if (fabricableItem == null) { return false; }
if (fabricableItem.RequiresRecipe)
if (fabricableItem.RequiresRecipe)
{
if (character == null) { return false; }
if (!AnyOneHasRecipeForItem(character, fabricableItem.TargetItem))
{
return false;
return true;
}
}
return false;
}
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
{
if (fabricableItem == null) { return false; }
if (MissingRequiredRecipe(fabricableItem, character)) { return false; }
if (fabricableItem.HideForNonTraitors)
{
@@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
hasPower = Voltage > MinVoltage;
hasPower = HasPower;
if (hasPower)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime);
@@ -46,7 +46,7 @@ namespace Barotrauma.Items.Components
if (item.CurrentHull == null) { return; }
if (Voltage < MinVoltage && PowerConsumption > 0)
if (!HasPower && PowerConsumption > 0)
{
return;
}
@@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components
}
}
public bool HasPower => IsActive && Voltage >= MinVoltage;
public override bool HasPower => IsActive && Voltage >= MinVoltage;
public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f;
private const float TinkeringSpeedIncrease = 4.0f;
@@ -140,13 +140,11 @@ namespace Barotrauma.Items.Components
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor;
currFlow = flowPercentage / 100.0f * MaxFlow * powerFactor;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
{
currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease;
}
currFlow = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpSpeed, currFlow);
//less effective when in a bad condition
@@ -80,6 +80,7 @@ namespace Barotrauma.Items.Components
private set
{
if (lastUser == value) { return; }
if (Screen.Selected.IsEditor) { return; }
lastUser = value;
if (lastUser == null)
{
@@ -246,6 +247,13 @@ namespace Barotrauma.Items.Components
}
}
//rapidly adjust the reactor in the first few seconds of the round to prevent overvoltages if the load changed between rounds
//(unless the reactor is being operated by a player)
if (GameMain.GameSession is { RoundDuration: <5 } && lastUser is not { IsPlayer: true })
{
UpdateAutoTemp(100.0f, (float)(Timing.Step * 10.0f));
}
#if CLIENT
if (PowerOn && AvailableFuel < 1)
{
@@ -340,7 +348,7 @@ namespace Barotrauma.Items.Components
{
foreach (Item item in containedItems)
{
if (!item.HasTag(Tags.Fuel)) { continue; }
if (!item.HasTag(Tags.ReactorFuel)) { continue; }
if (fissionRate > 0.0f)
{
bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost &&
@@ -707,13 +715,13 @@ namespace Barotrauma.Items.Components
var containObjective = AIContainItems<Reactor>(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true);
containObjective.Completed += ReportFuelRodCount;
containObjective.Abandoned += ReportFuelRodCount;
character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.Fuel, 30.0f);
character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.ReactorFuel, 30.0f);
void ReportFuelRodCount()
{
if (!character.IsOnPlayerTeam) { return; }
if (character.Submarine != Submarine.MainSub) { return; }
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.Fuel) && i.Condition > 1);
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.ReactorFuel) && i.Condition > 1);
if (remainingFuelRods == 0)
{
character.Speak(TextManager.Get("DialogOutOfFuelRods").Value, null, 0.0f, "outoffuelrods".ToIdentifier(), 30.0f);
@@ -8,6 +8,8 @@ namespace Barotrauma.Items.Components
{
partial class Sonar : Powered, IServerSerializable, IClientSerializable
{
public static List<Sonar> SonarList = new List<Sonar>();
public enum Mode
{
Active,
@@ -167,6 +169,7 @@ namespace Barotrauma.Items.Components
IsActive = true;
InitProjSpecific(element);
CurrentMode = Mode.Passive;
SonarList.Add(this);
}
partial void InitProjSpecific(ContentXElement element);
@@ -191,8 +194,7 @@ namespace Barotrauma.Items.Components
if (currentMode == Mode.Active)
{
if ((Voltage >= MinVoltage) &&
(!UseTransducers || connectedTransducers.Count > 0))
if (HasPower && (!UseTransducers || connectedTransducers.Count > 0))
{
if (currentPingIndex != -1)
{
@@ -380,6 +382,29 @@ namespace Barotrauma.Items.Components
}
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
#if CLIENT
sonarBlip?.Remove();
pingCircle?.Remove();
directionalPingCircle?.Remove();
screenOverlay?.Remove();
screenBackground?.Remove();
lineSprite?.Remove();
foreach (var t in targetIcons.Values)
{
t.Item1.Remove();
}
targetIcons.Clear();
MineralClusters = null;
#endif
SonarList.Remove(this);
}
public void ServerEventRead(IReadMessage msg, Client c)
{
bool isActive = msg.ReadBoolean();
@@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components
{
UpdateOnActiveEffects(deltaTime);
if (Voltage >= MinVoltage)
if (HasPower)
{
sendSignalTimer += deltaTime;
if (sendSignalTimer > SendSignalInterval)
@@ -61,6 +61,7 @@ namespace Barotrauma.Items.Components
private Sonar sonar;
private Submarine controlledSub;
public Submarine ControlledSub => controlledSub;
// AI interfacing
public Vector2 AITacticalTarget { get; set; }
@@ -75,6 +76,7 @@ namespace Barotrauma.Items.Components
private double lastReceivedSteeringSignalTime;
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)]
public bool AutoPilot
{
get { return autoPilot; }
@@ -298,7 +300,7 @@ namespace Barotrauma.Items.Components
controlledSub = sonar.ConnectedTransducers.Any() ? sonar.ConnectedTransducers.First().Item.Submarine : null;
}
if (Voltage < MinVoltage) { return; }
if (!HasPower) { return; }
if (user != null && user.Removed)
{
@@ -311,7 +313,7 @@ namespace Barotrauma.Items.Components
if (user != null && controlledSub != null &&
(user.SelectedItem == item || item.linkedTo.Contains(user.SelectedItem)))
{
userSkill = user.GetSkillLevel("helm") / 100.0f;
userSkill = user.GetSkillLevel(Tags.HelmSkill) / 100.0f;
}
// override autopilot pathing while the AI rams, and go full speed ahead