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

View File

@@ -63,6 +63,12 @@ namespace Barotrauma
private float prevZoom;
public float Shake;
/// <summary>
/// Should the camera's transform matrices be automatically updated to match the screen resolution?
/// </summary>
public bool AutoUpdateToScreenResolution = true;
public Vector2 ShakePosition { get; private set; }
private float shakeTimer;
@@ -198,10 +204,13 @@ namespace Barotrauma
public void UpdateTransform(bool interpolate = true, bool updateListener = true)
{
if (GameMain.GraphicsWidth != Resolution.X ||
GameMain.GraphicsHeight != Resolution.Y)
if (AutoUpdateToScreenResolution)
{
CreateMatrices();
if (GameMain.GraphicsWidth != Resolution.X ||
GameMain.GraphicsHeight != Resolution.Y)
{
CreateMatrices();
}
}
Vector2 interpolatedPosition = interpolate ? Timing.Interpolate(prevPosition, position) : position;

View File

@@ -42,13 +42,13 @@ namespace Barotrauma
if (wallTarget != null && !IsCoolDownRunning)
{
Vector2 wallTargetPos = wallTarget.Position;
if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.Position; }
if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.DrawPosition; }
wallTargetPos.Y = -wallTargetPos.Y;
GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false);
GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5);
}
GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity}", GUIStyle.Red, Color.Black);
GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"{targetValue.FormatZeroDecimal()} (M: {SelectedTargetMemory?.Priority.FormatZeroDecimal()}, P: {SelectedTargetingParams?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black);
GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"{targetValue.FormatZeroDecimal()} (M: {CurrentTargetMemory?.Priority.FormatZeroDecimal()}, P: {CurrentTargetingParams?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black);
}
/*GUIStyle.Font.DrawString(spriteBatch, targetValue.ToString(), pos - Vector2.UnitY * 80.0f, GUIStyle.Red);
@@ -73,7 +73,7 @@ namespace Barotrauma
}
GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 80.0f, State.ToString(), stateColor, Color.Black);
if (State == AIState.Attack && selectedTargetingParams != null && selectedTargetingParams.AttackPattern == AttackPattern.Circle)
if (State == AIState.Attack && currentTargetingParams != null && currentTargetingParams.AttackPattern == AttackPattern.Circle)
{
GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 100.0f, CirclePhase.ToString(), stateColor, Color.Black);
}
@@ -134,8 +134,8 @@ namespace Barotrauma
//GUI.DrawLine(spriteBatch, pos, ConvertUnits.ToDisplayUnits(steeringManager.AvoidLookAheadPos.X, -steeringManager.AvoidLookAheadPos.Y), Color.Orange, width: 4);
}
}
GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Steering.X, -Steering.Y)), Color.Blue, width: 4);
GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Character.AnimController.TargetMovement.X, -Character.AnimController.TargetMovement.Y)), Color.SteelBlue, width: 2);
GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Steering.X, -Steering.Y)), Color.Blue, width: 3);
}
}
}

View File

@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework;
using FarseerPhysics;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
@@ -11,7 +12,7 @@ namespace Barotrauma
{
if (Character == Character.Controlled) { return; }
if (!DebugAI) { return; }
Vector2 pos = Character.WorldPosition;
Vector2 pos = Character.DrawPosition;
pos.Y = -pos.Y;
Vector2 textOffset = new Vector2(-40, -160);
textOffset.Y -= Math.Max(ObjectiveManager.CurrentOrders.Count - 1, 0) * 20;
@@ -63,6 +64,25 @@ namespace Barotrauma
stringDrawPos += new Vector2(0, 20);
GUI.DrawString(spriteBatch, stringDrawPos, $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black);
}
if (currentObjective is AIObjectiveCombat
{
Weapon: Item weapon,
BlockedPositions: List<Vector2> blockedPositions
})
{
Vector2 weaponPos = weapon.DrawPosition;
weaponPos.Y = -weaponPos.Y;
foreach (Vector2 blockedPosition in blockedPositions)
{
Vector2 blockedPos = blockedPosition;
if (Character.Submarine != null)
{
blockedPos += Character.Submarine.DrawPosition;
}
blockedPos.Y = -blockedPos.Y;
GUI.DrawLine(spriteBatch, weaponPos, blockedPos, Color.Red);
}
}
}
Vector2 objectiveStringDrawPos = stringDrawPos + new Vector2(120, 40);

View File

@@ -23,13 +23,13 @@ namespace Barotrauma
partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos)
{
if (character != GameMain.Client.Character || !character.CanMove)
if (character != GameMain.Client.Character)
{
//remove states without a timestamp (there may still be ID-based states
//in the list when the controlled character switches to timestamp-based interpolation)
character.MemState.RemoveAll(m => m.Timestamp == 0.0f);
//use simple interpolation for other players' characters and characters that can't move
//use simple interpolation for other players' characters
if (character.MemState.Count > 0)
{
CharacterStateInfo serverPos = character.MemState.Last();
@@ -93,6 +93,9 @@ namespace Barotrauma
character.AnimController.Anim = AnimController.Animation.None;
}
character.AnimController.IgnorePlatforms = character.MemState[0].IgnorePlatforms;
character.AnimController.overrideTargetMovement = character.MemState[0].TargetMovement;
Vector2 newVelocity = Collider.LinearVelocity;
Vector2 newPosition = Collider.SimPosition;
float newRotation = Collider.Rotation;
@@ -103,16 +106,17 @@ namespace Barotrauma
{
newVelocity = newVelocity.ClampLength(100.0f);
if (!MathUtils.IsValid(newVelocity)) { newVelocity = Vector2.Zero; }
overrideTargetMovement = newVelocity.LengthSquared() > 0.01f ? newVelocity : Vector2.Zero;
Collider.LinearVelocity = newVelocity;
Collider.AngularVelocity = newAngularVelocity;
}
float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition);
float errorTolerance = character.CanMove && (!character.IsRagdolled || character.AnimController.IsHangingWithRope) ? 0.01f : 0.2f;
float errorTolerance =
ColliderControlsMovement && (!character.IsRagdolled || character.AnimController.IsHangingWithRope) ? 0.01f : 0.2f;
if (distSqrd > errorTolerance)
{
if (distSqrd > 10.0f || !character.CanMove)
character.AnimController.BodyInRest = false;
if (distSqrd > 10.0f)
{
Collider.TargetRotation = newRotation;
if (distSqrd > 10.0f)
@@ -126,30 +130,35 @@ namespace Barotrauma
}
}
SetPosition(newPosition, lerp: distSqrd < 5.0f, ignorePlatforms: false);
//make sure ragdoll isn't stuck at the wrong side of a platform if the movement is controlled by the ragdoll, and the ragdoll has come to rest server-side
if (!ColliderControlsMovement && newVelocity.LengthSquared() < 0.01f) { TryPlatformCorrection(newPosition); }
}
else
else if (ColliderControlsMovement)
{
Collider.TargetRotation = newRotation;
Collider.TargetPosition = newPosition;
Collider.MoveToTargetPosition(true);
}
}
//immobilized characters can't correct their position using AnimController movement
// -> we need to correct it manually
if (!character.CanMove)
{
float mainLimbDistSqrd = Vector2.DistanceSquared(MainLimb.PullJointWorldAnchorA, Collider.SimPosition);
float mainLimbErrorTolerance = 0.1f;
//if the main limb is roughly at the correct position and the collider isn't moving (much at least),
//don't attempt to correct the position.
if (mainLimbDistSqrd > mainLimbErrorTolerance || Collider.LinearVelocity.LengthSquared() > 0.05f)
else
{
MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
MainLimb.PullJointEnabled = true;
float mainLimbDistSqrd = Vector2.DistanceSquared(MainLimb.PullJointWorldAnchorA, newPosition);
float mainLimbErrorTolerance = character == GameMain.Client.Character ? 0.25f : 0.1f;
MainLimb.body.LinearVelocity = newVelocity;
//if the main limb is roughly at the correct position and the collider isn't moving (much at least),
//don't attempt to correct the position.
if (mainLimbDistSqrd > mainLimbErrorTolerance)
{
MainLimb.PullJointWorldAnchorB = newPosition;
MainLimb.PullJointEnabled = true;
if (!ColliderControlsMovement && newVelocity.LengthSquared() < 0.01f) { TryPlatformCorrection(newPosition); }
}
}
}
else if (!ColliderControlsMovement)
{
//correct velocity regardless of the positional error
MainLimb.body.LinearVelocity = newVelocity;
}
}
character.MemLocalState.Clear();
}
@@ -179,12 +188,14 @@ namespace Barotrauma
}
}
if (character.MemState.Count < 1) return;
if (character.MemState.Count < 1) { return; }
overrideTargetMovement = Vector2.Zero;
overrideTargetMovement = null;
CharacterStateInfo serverPos = character.MemState.Last();
Collider.LastServerState = serverPos;
if (!character.isSynced)
{
SetPosition(serverPos.Position, lerp: false);
@@ -282,19 +293,66 @@ namespace Barotrauma
}
else if (errorMagnitude > 0.01f)
{
Collider.TargetPosition = Collider.SimPosition + positionError;
Collider.TargetRotation = Collider.Rotation + rotationError;
Collider.MoveToTargetPosition(lerp: true);
if (ColliderControlsMovement)
{
Collider.TargetPosition = Collider.SimPosition + positionError;
Collider.TargetRotation = Collider.Rotation + rotationError;
Collider.MoveToTargetPosition(lerp: true);
}
else
{
float mainLimbErrorTolerance = character == GameMain.Client.Character ? 0.25f : 0.1f;
//if the main limb is roughly at the correct position and the collider isn't moving (much at least),
//don't attempt to correct the position.
if (errorMagnitude > mainLimbErrorTolerance)
{
MainLimb.PullJointWorldAnchorB = MainLimb.SimPosition + positionError;
MainLimb.PullJointEnabled = true;
if (serverPos.LinearVelocity.LengthSquared() < 0.01f) { TryPlatformCorrection(MainLimb.SimPosition + positionError); }
}
}
}
}
}
if (character.MemLocalState.Count > 120) character.MemLocalState.RemoveRange(0, character.MemLocalState.Count - 120);
if (character.MemLocalState.Count > 120) { character.MemLocalState.RemoveRange(0, character.MemLocalState.Count - 120); }
character.MemState.Clear();
}
}
/// <summary>
/// Attempts to correct the ragdoll to the correct side of a platform if the server position is above the platform and some of the ragdoll's limbs below it client-side, or vice versa.
/// </summary>
private void TryPlatformCorrection(Vector2 serverPos)
{
float highestPos = limbs.Where(static l => !l.IsSevered).Max(static l => l.SimPosition.Y);
highestPos = Math.Max(serverPos.Y, highestPos);
float lowestPos = limbs.Where(static l => !l.IsSevered).Min(static l => l.SimPosition.Y);
lowestPos = Math.Min(serverPos.Y, lowestPos);
var platform = Submarine.PickBody(new Vector2(serverPos.X, highestPos), new Vector2(serverPos.X, lowestPos), collisionCategory: Physics.CollisionPlatform, allowInsideFixture: true);
if (platform == null) { return; }
int serverDir = Math.Sign(serverPos.Y - platform.Position.Y);
foreach (var limb in limbs)
{
if (limb.IsSevered) { continue; }
int limbDir = Math.Sign(limb.SimPosition.Y - platform.Position.Y);
const float Margin = 0.01f;
if (limbDir != serverDir)
{
limb.body.SetTransformIgnoreContacts(
new Vector2(
limb.SimPosition.X,
serverDir > 0 ? Math.Max(serverPos.Y + Margin + limb.body.GetMaxExtent(), limb.SimPosition.Y) : Math.Min(serverPos.Y - Margin - limb.body.GetMaxExtent(), limb.SimPosition.Y)),
limb.Rotation);
}
}
}
partial void ImpactProjSpecific(float impact, Body body)
{
float volume = MathHelper.Clamp(impact - 3.0f, 0.5f, 1.0f);
@@ -563,15 +621,19 @@ namespace Barotrauma
void AdjustDepthOffset(Item item)
{
if (item?.GetComponent<Controller>() is { ControlCharacterPose: true, UserInCorrectPosition: true } controller && controller.User == character)
if (item == null) { return; }
foreach (var controller in item.GetComponents<Controller>())
{
if (controller.Item.SpriteDepth <= maxDepth || controller.DrawUserBehind)
if (controller is { ControlCharacterPose: true, UserInCorrectPosition: true } && controller.User == character)
{
depthOffset = Math.Max(controller.Item.GetDrawDepth() + 0.0001f - minDepth, -minDepth);
}
else
{
depthOffset = Math.Max(controller.Item.GetDrawDepth() - 0.0001f - maxDepth, 0.0f);
if (controller.Item.SpriteDepth <= maxDepth || controller.DrawUserBehind)
{
depthOffset = Math.Max(controller.Item.GetDrawDepth() + 0.0001f - minDepth, -minDepth);
}
else
{
depthOffset = Math.Max(controller.Item.GetDrawDepth() - 0.0001f - maxDepth, 0.0f);
}
}
}
}

View File

@@ -48,7 +48,7 @@ namespace Barotrauma
if (sound != null)
{
SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier());
SoundPlayer.PlaySound(sound, worldPosition);
}
}
}

View File

@@ -9,6 +9,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
@@ -27,7 +28,8 @@ namespace Barotrauma
protected float lastRecvPositionUpdateTime;
private float hudInfoHeight = 100.0f;
private const float DefaultHudInfoHeight = 78.0f;
private float hudInfoHeight = DefaultHudInfoHeight;
private List<CharacterSound> sounds;
@@ -249,7 +251,9 @@ namespace Barotrauma
public Vector2 Position;
public Vector2 DrawPosition;
public float MoveUpAmount;
public readonly string Text;
public readonly RichString Text;
public ImmutableArray<RichTextData>? RichTextData { get; private set; }
public readonly Character Character;
public readonly Submarine Submarine;
public readonly Vector2 TextSize;
@@ -259,8 +263,10 @@ namespace Barotrauma
public SpeechBubble(Character character, float lifeTime, Color color, string text = "")
{
Text = ToolBox.WrapText(text, GUI.IntScale(300), GUIStyle.SmallFont.GetFontForStr(text));
var richStr = RichString.Rich(text);
Text = ToolBox.WrapText(richStr.SanitizedValue, GUI.IntScale(300), GUIStyle.SmallFont.GetFontForStr(text));
TextSize = GUIStyle.SmallFont.MeasureString(Text);
RichTextData = richStr.RichTextData;
Character = character;
Position = GetDesiredPosition();
@@ -321,7 +327,6 @@ namespace Barotrauma
/// </summary>
public void ControlLocalPlayer(float deltaTime, Camera cam, bool moveCam = true)
{
if (DisableControls || GUI.InputBlockingMenuOpen)
{
foreach (Key key in keys)
@@ -329,7 +334,7 @@ namespace Barotrauma
if (key == null) { continue; }
key.Reset();
}
if (GUI.InputBlockingMenuOpen)
if (GUI.InputBlockingMenuOpen || ConversationAction.IsDialogOpen)
{
cursorPosition =
Position + PlayerInput.MouseSpeed.ClampLength(10.0f); //apply a little bit of movement to the cursor pos to prevent AFK kicking
@@ -416,6 +421,11 @@ namespace Barotrauma
UpdateLocalCursor(cam);
if (IsKeyHit(InputType.ToggleRun))
{
ToggleRun = !ToggleRun;
}
Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition);
if (GUI.PauseMenuOpen)
{
@@ -471,7 +481,7 @@ namespace Barotrauma
if (!GUI.InputBlockingMenuOpen)
{
if (SelectedItem != null &&
(SelectedItem.ActiveHUDs.Any(ic => ic.GuiFrame != null && HUD.CloseHUD(ic.GuiFrame.Rect)) ||
(SelectedItem.ActiveHUDs.Any(ic => ic.GuiFrame != null && ic.CloseByClickingOutsideGUIFrame && HUD.CloseHUD(ic.GuiFrame.Rect)) ||
((ViewTarget as Item)?.Prefab.FocusOnSelected ?? false) && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)))
{
if (GameMain.Client != null)
@@ -543,7 +553,10 @@ namespace Barotrauma
{
if (attackResult.Damage <= 1.0f) { return; }
}
PlaySound(CharacterSound.SoundType.Damage, maxInterval: 2);
if (AIState != AIState.PlayDead)
{
PlaySound(CharacterSound.SoundType.Damage, maxInterval: 2);
}
}
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log)
@@ -588,7 +601,6 @@ namespace Barotrauma
}
}
sounds.ForEach(s => s.Sound?.Dispose());
sounds.Clear();
if (GameMain.GameSession?.CrewManager != null &&
@@ -814,9 +826,12 @@ namespace Barotrauma
PlaySound(CharacterSound.SoundType.Idle);
}
break;
case AIState.PlayDead:
case AIState.Freeze:
case AIState.Hiding:
break;
default:
var petBehavior = enemyAI.PetBehavior;
if (petBehavior != null &&
if (enemyAI.PetBehavior is PetBehavior petBehavior &&
(petBehavior.Happiness < petBehavior.UnhappyThreshold || petBehavior.Hunger > petBehavior.HungryThreshold))
{
PlaySound(CharacterSound.SoundType.Unhappy);
@@ -948,7 +963,9 @@ namespace Barotrauma
Controlled != this &&
Submarine != null &&
Controlled.Submarine == Submarine &&
GameSettings.CurrentConfig.Graphics.LosMode != LosMode.None)
GameSettings.CurrentConfig.Graphics.LosMode != LosMode.None &&
//less restrictions on name tag visibility in PvP mode (always show them if the character is visible)
GameMain.GameSession?.GameMode is not PvPMode)
{
float yPos = Controlled.AnimController.FloorY - 1.5f;
@@ -965,15 +982,16 @@ namespace Barotrauma
Vector2 pos = DrawPosition;
pos.Y += hudInfoHeight;
if (CurrentHull != null && DrawPosition.Y > CurrentHull.WorldRect.Y - 130.0f)
float paddingBelowCeiling = 30.0f;
if (CurrentHull != null && DrawPosition.Y + DefaultHudInfoHeight > CurrentHull.WorldRect.Y - paddingBelowCeiling)
{
float lowerAmount = DrawPosition.Y - (CurrentHull.WorldRect.Y - 130.0f);
hudInfoHeight = MathHelper.Lerp(hudInfoHeight, 100.0f - lowerAmount, 0.1f);
float lowerAmount = (DrawPosition.Y + DefaultHudInfoHeight) - (CurrentHull.WorldRect.Y - paddingBelowCeiling);
hudInfoHeight = MathHelper.Lerp(hudInfoHeight, DefaultHudInfoHeight - lowerAmount, 0.1f);
hudInfoHeight = Math.Max(hudInfoHeight, 20.0f);
}
else
{
hudInfoHeight = MathHelper.Lerp(hudInfoHeight, 100.0f, 0.1f);
hudInfoHeight = MathHelper.Lerp(hudInfoHeight, DefaultHudInfoHeight, 0.1f);
}
pos.Y = -pos.Y;
@@ -1013,6 +1031,8 @@ namespace Barotrauma
CampaignInteractionType == CampaignMode.InteractionType.None ?
MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) :
1.0f;
//full name tag visibility in PvP mode to make it easier to tell who's an enemy
float nameTextAlpha = GameMain.GameSession?.GameMode is PvPMode ? 1.0f : hudInfoAlpha;
if (!GUI.DisableCharacterNames && hudInfoVisible &&
(controlled == null || this != controlled.FocusedCharacter || IsPet) && cam.Zoom > 0.4f)
@@ -1030,7 +1050,7 @@ namespace Barotrauma
}
Vector2 nameSize = GUIStyle.Font.MeasureString(name);
Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom;
Vector2 namePos = new Vector2(pos.X, pos.Y - 5.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom;
Color nameColor = GetNameColor();
Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
@@ -1057,7 +1077,7 @@ namespace Barotrauma
}
GUIStyle.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f);
GUIStyle.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f);
GUIStyle.Font.DrawString(spriteBatch, name, namePos, nameColor * nameTextAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f);
if (GameMain.DebugDraw)
{
GUIStyle.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White);
@@ -1068,15 +1088,18 @@ namespace Barotrauma
if (petBehavior != null && !IsDead && !IsUnconscious)
{
var petStatus = petBehavior.GetCurrentStatusIndicatorType();
var iconStyle = GUIStyle.GetComponentStyle("PetIcon." + petStatus);
if (iconStyle != null)
if (petStatus != PetBehavior.StatusIndicatorType.None)
{
Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f;
Vector2 iconPos = headPos;
iconPos.Y = -iconPos.Y;
var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First();
float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom;
icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale);
var iconStyle = GUIStyle.GetComponentStyle("PetIcon." + petStatus);
if (iconStyle != null)
{
Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f;
Vector2 iconPos = headPos;
iconPos.Y = -iconPos.Y;
var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First();
float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom;
icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale);
}
}
}
}
@@ -1100,7 +1123,7 @@ namespace Barotrauma
}
}
if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible)
if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible && AIState != AIState.PlayDead && AIState != AIState.Hiding)
{
hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f));
@@ -1175,7 +1198,7 @@ namespace Barotrauma
Vector2 bubbleSize = bubble.TextSize + Vector2.One * GUI.IntScale(15);
speechBubbleIconSliced.Draw(spriteBatch, new RectangleF(iconPos - bubbleSize / 2, bubbleSize), bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha);
}
GUI.DrawString(spriteBatch, iconPos - bubble.TextSize / 2, bubble.Text, bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha, font: GUIStyle.SmallFont);
GUI.DrawStringWithColors(spriteBatch, iconPos - bubble.TextSize / 2, bubble.Text.SanitizedValue, bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha, bubble.RichTextData, font: GUIStyle.SmallFont);
}
spriteBatch.End();
}
@@ -1233,7 +1256,7 @@ namespace Barotrauma
public Color GetNameColor()
{
CharacterTeamType team = teamID;
if (Info?.IsDisguisedAsAnother != null)
if (Info is { IsDisguisedAsAnother: true })
{
var idCard = Inventory.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent<IdCard>();
if (idCard != null)
@@ -1249,18 +1272,22 @@ namespace Barotrauma
}
}
CharacterTeamType myTeam =
Controlled?.TeamID ??
GameMain.Client?.MyClient?.TeamID ??
CharacterTeamType.Team1;
Color nameColor = GUIStyle.TextColorNormal;
if (Controlled != null && team != Controlled.TeamID)
if (TeamID == CharacterTeamType.FriendlyNPC)
{
if (TeamID == CharacterTeamType.FriendlyNPC)
{
nameColor = UniqueNameColor ?? Color.SkyBlue;
}
else
{
nameColor = GUIStyle.Red;
}
nameColor = UniqueNameColor ?? Color.SkyBlue;
}
else if (team != myTeam)
{
//opposing team is red when controlling a character
nameColor = GUIStyle.Red;
}
return nameColor;
}
@@ -1417,7 +1444,10 @@ namespace Barotrauma
partial void OnTalentGiven(TalentPrefab talentPrefab)
{
AddMessage(TextManager.Get("talentname." + talentPrefab.Identifier).Value, GUIStyle.Yellow, playSound: this == Controlled);
if (!talentPrefab.IsHiddenExtraTalent)
{
AddMessage(TextManager.Get("talentname." + talentPrefab.Identifier).Value, GUIStyle.Yellow, playSound: this == Controlled);
}
}
}
}

View File

@@ -11,17 +11,15 @@ namespace Barotrauma
{
partial class CharacterHUD
{
const float BossHealthBarDuration = 120.0f;
abstract class BossProgressBar
abstract class ProgressBar
{
public float FadeTimer;
public readonly GUIComponent TopContainer;
public readonly GUIComponent SideContainer;
public readonly GUIProgressBar TopHealthBar;
public readonly GUIProgressBar SideHealthBar;
public readonly GUIProgressBar TopBar;
public readonly GUIProgressBar SideBar;
public abstract bool Completed { get; }
@@ -33,9 +31,9 @@ namespace Barotrauma
public abstract Color Color { get; }
public BossProgressBar(LocalizedString label)
public ProgressBar(LocalizedString label, float fadeTimer = 120.0f)
{
FadeTimer = BossHealthBarDuration;
FadeTimer = fadeTimer;
TopContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.18f, 0.03f), HUDFrame.RectTransform, Anchor.TopCenter)
{
@@ -43,25 +41,25 @@ namespace Barotrauma
RelativeOffset = new Vector2(0.0f, 0.01f)
}, isHorizontal: false, childAnchor: Anchor.TopCenter);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), label, textAlignment: Alignment.Center, textColor: GUIStyle.Red);
TopHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.6f), TopContainer.RectTransform)
TopBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.6f), TopContainer.RectTransform)
{
MinSize = new Point(100, HUDLayoutSettings.HealthBarArea.Size.Y)
}, barSize: 0.0f, style: "CharacterHealthBarCentered")
{
Color = GUIStyle.Red
};
CreateNumberText(TopHealthBar);
CreateNumberText(TopBar);
SideContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), bossHealthContainer.RectTransform)
{
MinSize = new Point(80, 60)
}, isHorizontal: false, childAnchor: Anchor.TopRight);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), label, textAlignment: Alignment.CenterRight, textColor: GUIStyle.Red);
SideHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.7f), SideContainer.RectTransform), barSize: 0.0f, style: "CharacterHealthBar")
SideBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.7f), SideContainer.RectTransform), barSize: 0.0f, style: "CharacterHealthBar")
{
Color = GUIStyle.Red
};
CreateNumberText(SideHealthBar);
CreateNumberText(SideBar);
TopContainer.Visible = SideContainer.Visible = false;
TopContainer.CanBeFocused = false;
@@ -88,7 +86,7 @@ namespace Barotrauma
public abstract bool IsDuplicate(object targetObject);
}
class BossHealthBar : BossProgressBar
class HealthBar : ProgressBar
{
public readonly Character Character;
@@ -104,7 +102,7 @@ namespace Barotrauma
public override string NumberToDisplay => string.Empty;
public BossHealthBar(Character character) : base(character.DisplayName)
public HealthBar(Character character) : base(character.DisplayName)
{
Character = character;
}
@@ -115,7 +113,7 @@ namespace Barotrauma
}
}
class MissionProgressBar : BossProgressBar
class MissionProgressBar : ProgressBar
{
public readonly Mission Mission;
@@ -125,13 +123,13 @@ namespace Barotrauma
public override bool Interrupted => Mission.Failed || GameMain.GameSession?.Missions == null || !GameMain.GameSession.Missions.Contains(Mission);
public override Color Color => GUIStyle.Red;
public override Color Color => Mission.Prefab.ProgressBarColor;
public override string NumberToDisplay => Mission.Prefab.ShowProgressInNumbers ?
$"{Mission.State}/{Mission.Prefab.MaxProgressState}" :
string.Empty;
public MissionProgressBar(Mission mission) : base(mission.Prefab.ProgressBarLabel)
public MissionProgressBar(Mission mission) : base(mission.Prefab.ProgressBarLabel, fadeTimer: float.PositiveInfinity)
{
Mission = mission;
}
@@ -150,7 +148,7 @@ namespace Barotrauma
private static readonly List<Item> brokenItems = new List<Item>();
private static float brokenItemsCheckTimer;
private static readonly List<BossProgressBar> bossProgressBars = new List<BossProgressBar>();
private static readonly List<ProgressBar> bossProgressBars = new List<ProgressBar>();
private static readonly Dictionary<Identifier, LocalizedString> cachedHudTexts = new Dictionary<Identifier, LocalizedString>();
private static LanguageIdentifier cachedHudTextLanguage = LanguageIdentifier.None;
@@ -394,6 +392,7 @@ namespace Barotrauma
foreach (var target in mission.HudIconTargets)
{
if (target.Submarine != character.Submarine) { continue; }
if (target.Removed) { continue; }
float alpha = GetDistanceBasedIconAlpha(target, maxDistance: mission.Prefab.HudIconMaxDistance);
if (alpha <= 0.0f) { continue; }
GUI.DrawIndicator(spriteBatch, target.DrawPosition, cam, 100.0f, mission.Prefab.HudIcon, mission.Prefab.HudIconColor * alpha);
@@ -564,7 +563,7 @@ namespace Barotrauma
float alpha = MathHelper.Lerp(0.3f, 1.0f, distFactor);
GUI.DrawIndicator(
spriteBatch,
entity.WorldPosition,
entity.DrawPosition,
cam,
visibleRange,
style.GetDefaultSprite(),
@@ -592,7 +591,7 @@ namespace Barotrauma
if (Vector2.DistanceSquared(character.Position, item.Position) > 500f * 500f) { continue; }
var body = Submarine.CheckVisibility(character.SimPosition, item.SimPosition, ignoreLevel: true);
if (body != null && body.UserData as Item != item) { continue; }
GUI.DrawIndicator(spriteBatch, item.WorldPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Range<float>(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color, createOffset: false);
GUI.DrawIndicator(spriteBatch, item.DrawPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Range<float>(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color, createOffset: false);
}
}
@@ -752,7 +751,8 @@ namespace Barotrauma
}
textPos.X += 10.0f * GUI.Scale;
if (!character.FocusedCharacter.IsIncapacitated && character.FocusedCharacter.IsPet)
if (!character.FocusedCharacter.IsIncapacitated && character.FocusedCharacter.IsPet &&
character.FocusedCharacter.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior.CanPlayWith(character))
{
GUI.DrawString(spriteBatch, textPos, GetCachedHudText("PlayHint", InputType.Use),
GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont);
@@ -773,7 +773,7 @@ namespace Barotrauma
GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont);
textPos.Y += textSize.Y;
}
if (!character.FocusedCharacter.CustomInteractHUDText.IsNullOrEmpty() && character.FocusedCharacter.AllowCustomInteract)
if (character.FocusedCharacter.ShouldShowCustomInteractText)
{
GUI.DrawString(spriteBatch, textPos, character.FocusedCharacter.CustomInteractHUDText, GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont);
textPos.Y += textSize.Y;
@@ -784,7 +784,7 @@ namespace Barotrauma
{
if (character == null || character.IsDead || character.Removed) { return; }
if (bossProgressBars.Any(b => b.IsDuplicate(character))) { return; }
AddBossProgressBar(new BossHealthBar(character));
AddBossProgressBar(new HealthBar(character));
}
public static void ShowMissionProgressBar(Mission mission)
@@ -803,26 +803,26 @@ namespace Barotrauma
bossProgressBars.Clear();
}
private static void RemoveBossProgressBar(BossProgressBar progressBar)
private static void RemoveBossProgressBar(ProgressBar progressBar)
{
progressBar.SideContainer.Parent?.RemoveChild(progressBar.SideContainer);
progressBar.TopContainer.Parent?.RemoveChild(progressBar.TopContainer);
bossProgressBars.Remove(progressBar);
}
private static void AddBossProgressBar(BossProgressBar progressBar)
private static void AddBossProgressBar(ProgressBar progressBar)
{
var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars;
if (healthBarMode == EnemyHealthBarMode.HideAll)
if (healthBarMode == EnemyHealthBarMode.HideAll && progressBar is not MissionProgressBar)
{
return;
}
if (bossProgressBars.Count > 5)
{
BossProgressBar oldestHealthBar = bossProgressBars.First();
ProgressBar oldestHealthBar = bossProgressBars.First();
foreach (var bar in bossProgressBars)
{
if (bar.TopHealthBar.BarSize < oldestHealthBar.TopHealthBar.BarSize)
if (bar.TopBar.BarSize < oldestHealthBar.TopBar.BarSize)
{
oldestHealthBar = bar;
}
@@ -850,7 +850,7 @@ namespace Barotrauma
bossHealthBar.TopContainer.Visible = showTopBar;
bossHealthBar.SideContainer.Visible = !bossHealthBar.TopContainer.Visible;
bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = bossHealthBar.State;
bossHealthBar.TopBar.BarSize = bossHealthBar.SideBar.BarSize = bossHealthBar.State;
float alpha = Math.Min(bossHealthBar.FadeTimer, 1.0f);
if (bossHealthBar.TopContainer.Visible)
@@ -862,7 +862,7 @@ namespace Barotrauma
SetColor(bossHealthBar, bossHealthBar.SideContainer, alpha);
}
static void SetColor(BossProgressBar bossHealthBar, GUIComponent container, float alpha)
static void SetColor(ProgressBar bossHealthBar, GUIComponent container, float alpha)
{
foreach (var component in container.GetAllChildren())
{
@@ -887,7 +887,7 @@ namespace Barotrauma
for (int i = bossProgressBars.Count - 1; i >= 0 ; i--)
{
var bossHealthBar = bossProgressBars[i];
if (bossHealthBar.FadeTimer <= 0 || healthBarMode == EnemyHealthBarMode.HideAll)
if (bossHealthBar.FadeTimer <= 0 || (healthBarMode == EnemyHealthBarMode.HideAll && bossHealthBar is not MissionProgressBar))
{
bossHealthBar.SideContainer.Parent?.RemoveChild(bossHealthBar.SideContainer);
bossHealthBar.TopContainer.Parent?.RemoveChild(bossHealthBar.TopContainer);

View File

@@ -17,7 +17,8 @@ namespace Barotrauma
private static Sprite infoAreaPortraitBG;
public bool LastControlled;
public int CrewListIndex { get; set; } = -1;
public int CrewListIndex { get; set; } = int.MaxValue; //default to the bottom of the list
private Sprite disguisedPortrait;
private List<WearableSprite> disguisedAttachmentSprites;
@@ -32,6 +33,8 @@ namespace Barotrauma
private float tintHighlightThreshold;
private float tintHighlightMultiplier;
public bool ShowTalentResetPopupOnOpen = true;
public static void Init()
{
infoAreaPortraitBG = GUIStyle.GetComponentStyle("InfoAreaPortraitBG")?.GetDefaultSprite();
@@ -208,7 +211,7 @@ namespace Barotrauma
return frame;
}
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel)
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel, bool forceNotification)
{
if (TeamID == CharacterTeamType.FriendlyNPC) { return; }
if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; }
@@ -226,6 +229,18 @@ namespace Barotrauma
specialIncrease ? GUIStyle.Orange : GUIStyle.Green,
playSound: Character == Character.Controlled, skillIdentifier, increase);
}
else if (forceNotification)
{
float change = newLevel - prevLevel;
if (Math.Abs(change) > 0.01f)
{
string sign = change > 0 ? "+" : "-";
Character?.AddMessage(
$"{sign}{Math.Round(change, 2)} {TextManager.Get("SkillName." + skillIdentifier).Value}",
specialIncrease ? GUIStyle.Orange : GUIStyle.Green,
playSound: Character == Character.Controlled);
}
}
}
partial void OnExperienceChanged(int prevAmount, int newAmount)
@@ -511,6 +526,17 @@ namespace Barotrauma
else
{
origin = attachment.Sprite.Origin;
if (spriteEffects.HasFlag(SpriteEffects.FlipHorizontally))
{
origin.X = attachment.Sprite.size.X - origin.X;
}
if (spriteEffects.HasFlag(SpriteEffects.FlipVertically))
{
origin.Y = attachment.Sprite.size.Y - origin.Y;
}
//the portrait's origin is forced to 0,0 (presumably for easier drawing on the UI?), see LoadHeadElement
//we need to take that into account here and draw the attachment at where the origin of the "actual" head sprite would be
drawPos += HeadSprite.Origin * scale;
}
float depth = attachment.Sprite.Depth;
if (attachment.InheritLimbDepth)
@@ -526,6 +552,8 @@ namespace Barotrauma
string newName = inc.ReadString();
string originalName = inc.ReadString();
bool renamingEnabled = inc.ReadBoolean();
BotStatus botStatus = (BotStatus)inc.ReadByte();
int salary = inc.ReadInt32();
int tagCount = inc.ReadByte();
HashSet<Identifier> tagSet = new HashSet<Identifier>();
for (int i = 0; i < tagCount; i++)
@@ -576,6 +604,8 @@ namespace Barotrauma
MinReputationToHire = (factionId, minReputationToHire),
RenamingEnabled = renamingEnabled
};
ch.BotStatus = botStatus;
ch.Salary = salary;
ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
ch.Head.SkinColor = skinColor;
ch.Head.HairColor = hairColor;
@@ -586,6 +616,8 @@ namespace Barotrauma
ch.ExperiencePoints = inc.ReadInt32();
ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints);
ch.PermanentlyDead = inc.ReadBoolean();
ch.TalentRefundPoints = inc.ReadInt32();
ch.TalentResetCount = inc.ReadInt32();
return ch;
}

View File

@@ -47,6 +47,7 @@ namespace Barotrauma
SelectedCharacter,
SelectedItem,
SelectedSecondaryItem,
AnimController.TargetMovement,
AnimController.Anim);
memLocalState.Add(posInfo);
@@ -56,7 +57,7 @@ namespace Barotrauma
if (IsKeyDown(InputType.Right)) newInput |= InputNetFlags.Right;
if (IsKeyDown(InputType.Up)) newInput |= InputNetFlags.Up;
if (IsKeyDown(InputType.Down)) newInput |= InputNetFlags.Down;
if (IsKeyDown(InputType.Run)) newInput |= InputNetFlags.Run;
if (IsKeyDown(InputType.Run) || ToggleRun) newInput |= InputNetFlags.Run;
if (IsKeyDown(InputType.Crouch)) newInput |= InputNetFlags.Crouch;
if (IsKeyHit(InputType.Select)) newInput |= InputNetFlags.Select; //TODO: clean up the way this input is registered
if (IsKeyHit(InputType.Deselect)) newInput |= InputNetFlags.Deselect;
@@ -68,7 +69,7 @@ namespace Barotrauma
if (IsKeyDown(InputType.Attack)) newInput |= InputNetFlags.Attack;
if (IsKeyDown(InputType.Ragdoll)) newInput |= InputNetFlags.Ragdoll;
if (AnimController.TargetDir == Direction.Left) newInput |= InputNetFlags.FacingLeft;
if (AnimController.Dir < 0) newInput |= InputNetFlags.FacingLeft;
Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
relativeCursorPos.Normalize();
@@ -154,6 +155,9 @@ namespace Barotrauma
case TreatmentEventData _:
msg.WriteBoolean(AnimController.Anim == AnimController.Animation.CPR);
break;
case ConfirmRefundEventData _:
//do nothing
break;
case CharacterStatusEventData _:
//do nothing
break;
@@ -202,12 +206,16 @@ namespace Barotrauma
keys[(int)InputType.Use].Held = useInput;
keys[(int)InputType.Use].SetState(false, useInput);
bool crouching = msg.ReadBoolean();
if (AnimController is HumanoidAnimController)
{
bool crouching = msg.ReadBoolean();
keys[(int)InputType.Crouch].Held = crouching;
keys[(int)InputType.Crouch].SetState(false, crouching);
}
else if (AnimController is FishAnimController fishAnim)
{
fishAnim.Reverse = msg.ReadBoolean();
}
bool attackInput = msg.ReadBoolean();
keys[(int)InputType.Attack].Held = attackInput;
@@ -255,17 +263,22 @@ namespace Barotrauma
msg.ReadRangedSingle(-MaxVel, MaxVel, 12));
linearVelocity = NetConfig.Quantize(linearVelocity, -MaxVel, MaxVel, 12);
Vector2 targetMovement = new Vector2(
msg.ReadRangedSingle(-Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12),
msg.ReadRangedSingle(-Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12));
targetMovement = NetConfig.Quantize(targetMovement, -Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12);
bool fixedRotation = msg.ReadBoolean();
float? rotation = null;
float? angularVelocity = null;
if (!fixedRotation)
{
rotation = msg.ReadSingle();
float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity;
angularVelocity = msg.ReadRangedSingle(-MaxAngularVel, MaxAngularVel, 8);
angularVelocity = NetConfig.Quantize(angularVelocity.Value, -MaxAngularVel, MaxAngularVel, 8);
angularVelocity = msg.ReadSingle();
}
bool ignorePlatforms = msg.ReadBoolean();
bool readStatus = msg.ReadBoolean();
if (readStatus)
{
@@ -287,7 +300,7 @@ namespace Barotrauma
{
byte happiness = msg.ReadByte();
byte hunger = msg.ReadByte();
if ((AIController as EnemyAIController)?.PetBehavior is PetBehavior petBehavior)
if (AIController is EnemyAIController { PetBehavior: PetBehavior petBehavior })
{
petBehavior.Happiness = (float)happiness / byte.MaxValue * petBehavior.MaxHappiness;
petBehavior.Hunger = (float)hunger / byte.MaxValue * petBehavior.MaxHunger;
@@ -303,13 +316,13 @@ namespace Barotrauma
msg.ReadPadBits();
int index = 0;
if (GameMain.Client.Character == this && CanMove)
if (GameMain.Client.Character == this)
{
var posInfo = new CharacterStateInfo(
pos, rotation,
networkUpdateID,
facingRight ? Direction.Right : Direction.Left,
selectedCharacter, selectedItem, selectedSecondaryItem, animation);
selectedCharacter, selectedItem, selectedSecondaryItem, targetMovement, animation, ignorePlatforms);
while (index < memState.Count && NetIdUtils.IdMoreRecent(posInfo.ID, memState[index].ID))
index++;
@@ -321,7 +334,7 @@ namespace Barotrauma
pos, rotation,
linearVelocity, angularVelocity,
sendingTime, facingRight ? Direction.Right : Direction.Left,
selectedCharacter, selectedItem, selectedSecondaryItem, animation);
selectedCharacter, selectedItem, selectedSecondaryItem, targetMovement, animation, ignorePlatforms);
while (index < memState.Count && posInfo.Timestamp > memState[index].Timestamp)
index++;
@@ -371,6 +384,9 @@ namespace Barotrauma
GameMain.Client.HasSpawned = true;
GameMain.Client.Character = this;
GameMain.LightManager.LosEnabled = true;
#if DEBUG
GameMain.LightManager.LosEnabled = !GameMain.DevMode;
#endif
GameMain.LightManager.LosAlpha = 1f;
GameMain.Client.WaitForNextRoundRespawn = null;
}
@@ -393,14 +409,15 @@ namespace Barotrauma
break;
case EventType.Status:
ReadStatus(msg);
GodMode = msg.ReadBoolean();
break;
case EventType.UpdateSkills:
int skillCount = msg.ReadByte();
for (int i = 0; i < skillCount; i++)
Identifier skillIdentifier = msg.ReadIdentifier();
if (!skillIdentifier.IsEmpty)
{
Identifier skillIdentifier = msg.ReadIdentifier();
bool forceNotification = msg.ReadBoolean();
float skillLevel = msg.ReadSingle();
info?.SetSkillLevel(skillIdentifier, skillLevel);
info?.SetSkillLevel(skillIdentifier, skillLevel, forceNotification: forceNotification);
}
break;
case EventType.SetAttackTarget:
@@ -512,7 +529,12 @@ namespace Barotrauma
break;
case EventType.UpdateExperience:
int experienceAmount = msg.ReadInt32();
info?.SetExperience(experienceAmount);
int additionalTalentPoints = msg.ReadInt32();
if (info != null)
{
info.SetExperience(experienceAmount);
info.AdditionalTalentPoints = additionalTalentPoints;
}
break;
case EventType.UpdateTalents:
ushort talentCount = msg.ReadUInt16();
@@ -527,6 +549,20 @@ namespace Barotrauma
int moneyAmount = msg.ReadInt32();
SetMoney(moneyAmount);
break;
case EventType.UpdateTalentRefundPoints:
int refundPoints = msg.ReadInt32();
if (info != null)
{
if (refundPoints > info.TalentRefundPoints)
{
info.ShowTalentResetPopupOnOpen = true;
}
info.TalentRefundPoints = refundPoints;
}
break;
case EventType.ConfirmTalentRefund:
Info?.RefundTalents();
break;
case EventType.UpdatePermanentStats:
byte savedStatValueCount = msg.ReadByte();
StatTypes statType = (StatTypes)msg.ReadByte();
@@ -730,7 +766,7 @@ namespace Barotrauma
if (character.IsHuman && character.TeamID != CharacterTeamType.FriendlyNPC && character.TeamID != CharacterTeamType.None)
{
CharacterInfo duplicateCharacterInfo = GameMain.GameSession.CrewManager.GetCharacterInfos().FirstOrDefault(c => c.ID == info.ID);
CharacterInfo duplicateCharacterInfo = GameMain.GameSession.CrewManager.GetCharacterInfos(includeReserveBench: true).FirstOrDefault(c => c.ID == info.ID);
GameMain.GameSession.CrewManager.RemoveCharacterInfo(duplicateCharacterInfo);
if (character.isDead)
{
@@ -750,6 +786,9 @@ namespace Barotrauma
if (!character.IsDead) { Controlled = character; }
GameMain.LightManager.LosEnabled = true;
#if DEBUG
GameMain.LightManager.LosEnabled = !GameMain.DevMode;
#endif
GameMain.LightManager.LosAlpha = 1f;
GameMain.NetLobbyScreen.CampaignCharacterDiscarded = false;
@@ -817,6 +856,7 @@ namespace Barotrauma
if (IsDead) { Revive(); }
CharacterHealth.ClientRead(msg);
}
byte severedLimbCount = msg.ReadByte();
for (int i = 0; i < severedLimbCount; i++)
{

View File

@@ -180,7 +180,7 @@ namespace Barotrauma
fakeBrokenTimer -= deltaTime;
if (fakeBrokenTimer > 0.0f) { return; }
foreach (Item item in Item.ItemList)
foreach (Item item in Item.RepairableItems)
{
var repairable = item.GetComponent<Repairable>();
if (repairable == null) { continue; }

View File

@@ -32,7 +32,6 @@ namespace Barotrauma
public static Sprite DamageOverlay => DamageOverlayPrefab.Prefabs.ActivePrefab.DamageOverlay;
private Point screenResolution;
private float uiScale, inventoryScale;
@@ -105,6 +104,12 @@ namespace Barotrauma
private GUILayoutGroup treatmentLayout;
private GUIListBox recommendedTreatmentContainer;
/// <summary>
/// Timer for updating visuals (limb tints and overlays) caused by the affliction
/// </summary>
private float updateVisualsTimer = Rand.Range(0.0f, UpdateVisualsInterval);
const float UpdateVisualsInterval = 0.5f;
private float distortTimer;
// 0-1
@@ -461,15 +466,17 @@ namespace Barotrauma
private void OnAttacked(Character attacker, AttackResult attackResult)
{
if (Math.Abs(attackResult.Damage) < 0.01f) { return; }
DamageOverlayTimer = MathHelper.Clamp(attackResult.Damage / MaxVitality, DamageOverlayTimer, 1.0f);
if (healthShadowDelay <= 0.0f) { healthShadowDelay = 1.0f; }
if (ShowDamageOverlay)
{
DamageOverlayTimer = MathHelper.Clamp(attackResult.Damage / MaxVitality, DamageOverlayTimer, 1.0f);
float additionalIntensity = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 0.1f, attackResult.Damage / MaxVitality));
damageIntensity = MathHelper.Clamp(damageIntensity + additionalIntensity, 0, 1);
}
if (healthShadowDelay <= 0.0f) { healthShadowDelay = 1.0f; }
if (healthBarPulsateTimer <= 0.0f) { healthBarPulsatePhase = 0.0f; }
healthBarPulsateTimer = 1.0f;
float additionalIntensity = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 0.1f, attackResult.Damage / MaxVitality));
damageIntensity = MathHelper.Clamp(damageIntensity + additionalIntensity, 0, 1);
DisplayVitalityDelay = 0.5f;
}
@@ -1036,6 +1043,12 @@ namespace Barotrauma
var affliction = kvp.Key;
affliction.Prefab.AfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f,
new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y));
var activeEffect = affliction.GetActiveEffect();
if (activeEffect is { ThermalOverlayRange: > 0.0f })
{
StatusHUD.DrawThermalOverlay(spriteBatch, Character, Character, activeEffect.ThermalOverlayColor, activeEffect.ThermalOverlayRange, effectState: (float)Timing.TotalTimeUnpaused, showDeadCharacters: false);
}
}
float damageOverlayAlpha = DamageOverlayTimer;
@@ -1130,6 +1143,8 @@ namespace Barotrauma
if (!statusIconVisibleTime.ContainsKey(afflictionPrefab)) { statusIconVisibleTime.Add(afflictionPrefab, 0.0f); }
statusIconVisibleTime[afflictionPrefab] += deltaTime;
Color color = GetAfflictionIconColor(afflictionPrefab, affliction);
var matchingIcon =
afflictionIconContainer.GetChildByUserData(afflictionPrefab) ??
hiddenAfflictionIconContainer.GetChildByUserData(afflictionPrefab);
@@ -1138,9 +1153,13 @@ namespace Barotrauma
matchingIcon = new GUIButton(new RectTransform(new Point(afflictionIconContainer.Rect.Height), afflictionIconContainer.RectTransform), style: null)
{
UserData = afflictionPrefab,
ToolTip = affliction.Prefab.Name,
ToolTip = $"‖color:{color.ToStringHex()}‖{affliction.Prefab.Name}‖color:end‖",
CanBeSelected = false
};
if (affliction.Prefab.ShowDescriptionInTooltip)
{
matchingIcon.ToolTip = matchingIcon.ToolTip + "\n" + affliction.Prefab.GetDescription(affliction.Strength, AfflictionPrefab.Description.TargetType.Self);
}
if (affliction == pressureAffliction)
{
matchingIcon.ToolTip = TextManager.Get("PressureHUDWarning");
@@ -1149,6 +1168,8 @@ namespace Barotrauma
{
matchingIcon.ToolTip = TextManager.Get("OxygenHUDWarning");
}
matchingIcon.ToolTip = RichString.Rich(matchingIcon.ToolTip);
new GUIImage(new RectTransform(Vector2.One, matchingIcon.RectTransform, Anchor.BottomCenter), afflictionPrefab.Icon, scaleToFit: true)
{
CanBeFocused = false
@@ -1159,7 +1180,7 @@ namespace Barotrauma
matchingIcon.RectTransform.Parent = hiddenAfflictionIconContainer.RectTransform;
}
var image = matchingIcon.GetChild<GUIImage>();
image.Color = GetAfflictionIconColor(afflictionPrefab, affliction);
image.Color = color;
image.HoverColor = Color.Lerp(image.Color, Color.White, 0.5f);
if (affliction.DamagePerSecond > 1.0f && matchingIcon.FlashTimer <= 0.0f)
@@ -1380,7 +1401,7 @@ namespace Barotrauma
recommendedTreatmentContainer.Content.ClearChildren();
float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel("medical");
float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel(Tags.MedicalSkill);
//key = item identifier
//float = suitability
@@ -1388,7 +1409,9 @@ namespace Barotrauma
GetSuitableTreatments(treatmentSuitability,
user: Character.Controlled,
ignoreHiddenAfflictions: true,
limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex));
limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex),
checkTreatmentSuggestionThreshold: true,
checkTreatmentThreshold: false);
foreach (Identifier treatment in treatmentSuitability.Keys.ToList())
{
@@ -1949,7 +1972,6 @@ namespace Barotrauma
}
}
private bool ShouldDisplayAfflictionOnLimb(KeyValuePair<Affliction, LimbHealth> kvp, LimbHealth limbHealth)
{
if (!kvp.Key.ShouldShowIcon(Character)) { return false; }
@@ -2058,23 +2080,23 @@ namespace Barotrauma
newAfflictions.Add((limbHealths[limbIndex], afflictionPrefab, afflictionStrength));
}
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions)
{
//deactivate afflictions that weren't included in the network message
if (!newAfflictions.Any(a => kvp.Key.Prefab == a.afflictionPrefab && kvp.Value == a.limb))
if (newAfflictions.None(a => affliction.Prefab == a.afflictionPrefab && limbHealth == a.limb))
{
kvp.Key.Strength = 0.0f;
affliction.Strength = 0.0f;
}
}
foreach (var (limb, afflictionPrefab, strength) in newAfflictions)
{
Affliction existingAffliction = null;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions)
{
if (kvp.Key.Prefab == afflictionPrefab && kvp.Value == limb)
if (affliction.Prefab == afflictionPrefab && limbHealth == limb)
{
existingAffliction = kvp.Key;
existingAffliction = affliction;
break;
}
}
@@ -2123,9 +2145,8 @@ namespace Barotrauma
if (!Character.Params.Health.ApplyAfflictionColors) { return; }
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
foreach ((Affliction affliction, LimbHealth _) in afflictions)
{
var affliction = kvp.Key;
Color faceTint = affliction.GetFaceTint();
if (faceTint.A > FaceTint.A) { FaceTint = faceTint; }
Color bodyTint = affliction.GetBodyTint();
@@ -2137,17 +2158,23 @@ namespace Barotrauma
{
foreach (Limb limb in Character.AnimController.Limbs)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { continue; }
limb.BurnOverlayStrength = 0.0f;
limb.DamageOverlayStrength = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
}
foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions)
{
if (affliction.Prefab.BurnOverlayAlpha <= 0.0f && affliction.Prefab.DamageOverlayAlpha <= 0.0f) { continue; }
float burnStrength = affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.BurnOverlayAlpha;
float damageOverlayStrength = affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.DamageOverlayAlpha;
foreach (Limb limb in Character.AnimController.Limbs)
{
var affliction = kvp.Key;
float burnStrength = affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.BurnOverlayAlpha;
if (kvp.Value == limbHealths[limb.HealthIndex] || !affliction.Prefab.LimbSpecific)
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { continue; }
if (limbHealth == limbHealths[limb.HealthIndex] || !affliction.Prefab.LimbSpecific)
{
limb.BurnOverlayStrength += burnStrength;
limb.DamageOverlayStrength += affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.DamageOverlayAlpha;
limb.DamageOverlayStrength += damageOverlayStrength;
}
else
{

View File

@@ -17,13 +17,16 @@ public static class InteractionLabelManager
public RectangleF TextRect { get; set; }
public RichString Text;
public readonly Vector2 OriginalItemPosition;
public bool OverlapPreventionDone;
public LabelData(Item item, RectangleF textRect, Camera drawCamera)
public LabelData(Item item, RectangleF textRect, RichString text, Camera drawCamera)
{
Item = item;
Text = text;
TextRect = textRect;
OriginalItemPosition = item.Position;
this.drawCamera = drawCamera;
@@ -106,7 +109,7 @@ public static class InteractionLabelManager
if (labels.None(l => l.Item == interactableInRange))
{
var labelData = new LabelData(interactableInRange, textRect, cam);
var labelData = new LabelData(interactableInRange, textRect, RichString.Rich(interactableInRange.Prefab.Name), cam);
labels.Add(labelData);
}
}
@@ -124,7 +127,7 @@ public static class InteractionLabelManager
private static RectangleF GetLabelRect(Item item, Camera cam)
{
// create rectangle for overlap prevention
Vector2 itemTextSizeScreen = GUIStyle.SubHeadingFont.MeasureString(item.Name) * LabelScale;
Vector2 itemTextSizeScreen = GUIStyle.SubHeadingFont.MeasureString(RichString.Rich(item.Prefab.Name).SanitizedValue) * LabelScale;
Vector2 interactablePosScreen = cam.WorldToScreen(item.Position);
RectangleF textRect = new RectangleF(interactablePosScreen.X, interactablePosScreen.Y, itemTextSizeScreen.X, itemTextSizeScreen.Y);
// center the rectangle on the item
@@ -320,9 +323,11 @@ public static class InteractionLabelManager
GUIStyle.InteractionLabelBackground.Draw(spriteBatch, backgroundRect, color * 0.7f);
GUIStyle.SubHeadingFont.DrawString(spriteBatch,
labelData.Item.Name,
textDrawPosScreen, color, rotation: 0, origin: Vector2.Zero, scale, spriteEffects: SpriteEffects.None, layerDepth: 0.0f,
GUIStyle.SubHeadingFont.DrawStringWithColors(spriteBatch,
labelData.Text.SanitizedValue,
textDrawPosScreen, color, rotation: 0, origin: Vector2.Zero, scale, spriteEffects: SpriteEffects.None,
layerDepth: 0.0f,
richTextData: labelData.Text.RichTextData,
forceUpperCase: ForceUpperCase.No);
}
}

View File

@@ -1,85 +1,75 @@
using Microsoft.Xna.Framework;
using System.Linq;
using System;
using System.Xml.Linq;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class JobPrefab : PrefabWithUintIdentifier
{
public GUIButton CreateInfoFrame(out GUIComponent buttonContainer)
public GUIButton CreateInfoFrame(bool isPvP, out GUIComponent buttonContainer)
{
int width = 500, height = 400;
int windowPixelWidth = 500, windowPixelHeight = 400;
Point absoluteWindowSize = new Point((int)(windowPixelWidth * GUI.xScale), (int)(windowPixelHeight * GUI.yScale));
GUIButton frameHolder = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null);
new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, frameHolder.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");
GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, height), frameHolder.RectTransform, Anchor.Center));
GUIFrame frame = new GUIFrame(new RectTransform(absoluteWindowSize, frameHolder.RectTransform, Anchor.Center));
GUIFrame paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), style: null);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform), Name, font: GUIStyle.LargeFont);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform), Name, font: GUIStyle.LargeFont)
{
CanBeFocused = false
};
var descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.15f) },
Description, font: GUIStyle.SmallFont, wrap: true);
var contentList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) })
{
ScrollBarVisible = true,
AutoHideScrollBar = true,
CurrentSelectMode = GUIListBox.SelectMode.None,
Padding = new Vector4(0, GUI.Scale * 10, 0, 0),
Spacing = (int)(GUI.Scale * 5)
};
var descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), contentList.Content.RectTransform),
Description, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft)
{
CanBeFocused = false,
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), contentList.Content.RectTransform),
TextManager.Get("Skills"), font: GUIStyle.LargeFont)
{
CanBeFocused = false
};
var skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 0.5f), paddedFrame.RectTransform)
{ RelativeOffset = new Vector2(0.0f, 0.2f + descriptionBlock.RectTransform.RelativeSize.Y) });
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillContainer.RectTransform),
TextManager.Get("Skills"), font: GUIStyle.LargeFont);
foreach (SkillPrefab skill in Skills)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillContainer.RectTransform),
" - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), (int)skill.LevelRange.Start + " - " + (int)skill.LevelRange.End),
font: GUIStyle.SmallFont);
var levelRange = skill.GetLevelRange(isPvP);
string levelStr =
levelRange.End > levelRange.Start ?
(int)levelRange.Start + " - " + (int)levelRange.End :
((int)levelRange.Start).ToString();
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), contentList.Content.RectTransform),
" - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), levelStr),
font: GUIStyle.SmallFont, wrap: true)
{
CanBeFocused = false
};
}
buttonContainer = paddedFrame;
/*if (!ItemIdentifiers.TryGetValue(variant, out var itemIdentifiers)) { return backFrame; }
var itemContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 0.5f), paddedFrame.RectTransform, Anchor.TopRight)
{ RelativeOffset = new Vector2(0.0f, 0.2f + descriptionBlock.RectTransform.RelativeSize.Y) })
{
Stretch = true
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), itemContainer.RectTransform),
TextManager.Get("Items", "mapentitycategory.equipment"), font: GUIStyle.LargeFont);
foreach (string identifier in itemIdentifiers.Distinct())
{
if (!(MapEntityPrefab.Find(name: null, identifier: identifier) is ItemPrefab itemPrefab)) { continue; }
int count = itemIdentifiers.Count(i => i == identifier);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), itemContainer.RectTransform),
" - " + (count == 1 ? itemPrefab.Name : itemPrefab.Name + " x" + count),
font: GUIStyle.SmallFont);
}*/
return frameHolder;
}
public class OutfitPreview
public IEnumerable<Sprite> GetJobOutfitSprites(CharacterTeamType team, bool isPvPMode)
{
public readonly List<(Sprite sprite, Vector2 drawOffset)> Sprites;
public Vector2 Dimensions;
public OutfitPreview()
{
Sprites = new List<(Sprite sprite, Vector2 drawOffset)>();
Dimensions = Vector2.One;
}
public void AddSprite(Sprite sprite, Vector2 drawOffset)
{
Sprites.Add((sprite, drawOffset));
}
}
public List<OutfitPreview> GetJobOutfitSprites(CharacterInfoPrefab charInfoPrefab, bool useInventoryIcon, out Vector2 maxDimensions)
{
List<OutfitPreview> outfitPreviews = new List<OutfitPreview>();
maxDimensions = Vector2.One;
var equipIdentifiers = Element.GetChildElements("ItemSet").Elements().Where(e => e.GetAttributeBool("outfit", false)).Select(e => e.GetAttributeIdentifier("identifier", ""));
var equipIdentifiers = JobItems
.SelectMany(kvp => kvp.Value)
.Where(j => j.Outfit)
.Select(j => j.GetItemIdentifier(team, isPvPMode));
List<ItemPrefab> outfitPrefabs = new List<ItemPrefab>();
foreach (var equipIdentifier in equipIdentifiers)
@@ -88,45 +78,9 @@ namespace Barotrauma
if (itemPrefab != null) { outfitPrefabs.Add(itemPrefab); }
}
if (!outfitPrefabs.Any()) { return null; }
if (!outfitPrefabs.Any()) { return Enumerable.Empty<Sprite>(); }
for (int i = 0; i < outfitPrefabs.Count; i++)
{
var outfitPreview = new OutfitPreview();
if (!ItemSets.TryGetValue(i, out var itemSetElement)) { continue; }
var previewElement = itemSetElement.GetChildElement("PreviewSprites");
if (previewElement == null || useInventoryIcon)
{
if (outfitPrefabs[i] is ItemPrefab prefab && prefab.InventoryIcon != null)
{
outfitPreview.AddSprite(prefab.InventoryIcon, Vector2.Zero);
outfitPreview.Dimensions = prefab.InventoryIcon.SourceRect.Size.ToVector2();
maxDimensions.X = MathHelper.Max(maxDimensions.X, outfitPreview.Dimensions.X);
maxDimensions.Y = MathHelper.Max(maxDimensions.Y, outfitPreview.Dimensions.Y);
}
outfitPreviews.Add(outfitPreview);
continue;
}
var children = previewElement.Elements().ToList();
for (int n = 0; n < children.Count; n++)
{
var spriteElement = children[n];
string spriteTexture = charInfoPrefab.ReplaceVars(spriteElement.GetAttributeString("texture", ""), charInfoPrefab.Heads.First());
var sprite = new Sprite(spriteElement, file: spriteTexture);
sprite.size = new Vector2(sprite.SourceRect.Width, sprite.SourceRect.Height);
outfitPreview.AddSprite(sprite, children[n].GetAttributeVector2("offset", Vector2.Zero));
}
outfitPreview.Dimensions = previewElement.GetAttributeVector2("dims", Vector2.One);
maxDimensions.X = MathHelper.Max(maxDimensions.X, outfitPreview.Dimensions.X);
maxDimensions.Y = MathHelper.Max(maxDimensions.Y, outfitPreview.Dimensions.Y);
outfitPreviews.Add(outfitPreview);
}
return outfitPreviews;
return outfitPrefabs.Select(p => p.InventoryIcon ?? p.Sprite);
}
}
}

View File

@@ -137,7 +137,16 @@ namespace Barotrauma
{
get
{
var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.Exclusive && c.IsActive && c.DeformableSprite != null);
// Performance-sensitive, hence implemented without Linq.
ConditionalSprite conditionalSprite = null;
foreach (ConditionalSprite cs in ConditionalSprites)
{
if (cs.Exclusive && cs.IsActive && cs.DeformableSprite != null)
{
conditionalSprite = cs;
break;
}
}
if (conditionalSprite != null)
{
return conditionalSprite.DeformableSprite;
@@ -155,7 +164,16 @@ namespace Barotrauma
{
get
{
var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.Exclusive && c.IsActive && c.ActiveSprite != null);
// Performance-sensitive, hence implemented without Linq.
ConditionalSprite conditionalSprite = null;
foreach (ConditionalSprite cs in ConditionalSprites)
{
if (cs.Exclusive && cs.IsActive && cs.ActiveSprite != null)
{
conditionalSprite = cs;
break;
}
}
if (conditionalSprite != null)
{
return conditionalSprite.ActiveSprite;
@@ -184,12 +202,6 @@ namespace Barotrauma
public Sprite DamagedSprite { get; private set; }
public bool Hide
{
get => Params.Hide;
set => Params.Hide = value;
}
public List<ConditionalSprite> ConditionalSprites { get; private set; } = new List<ConditionalSprite>();
private Dictionary<DecorativeSprite, SpriteState> spriteAnimState = new Dictionary<DecorativeSprite, SpriteState>();
private Dictionary<int, List<DecorativeSprite>> DecorativeSpriteGroups = new Dictionary<int, List<DecorativeSprite>>();
@@ -198,6 +210,7 @@ namespace Barotrauma
{
public float RotationState;
public float OffsetState;
public float ScaleState;
public Vector2 RandomOffsetMultiplier = new Vector2(Rand.Range(-1.0f, 1.0f), Rand.Range(-1.0f, 1.0f));
public float RandomRotationFactor = Rand.Range(0.0f, 1.0f);
public float RandomScaleFactor = Rand.Range(0.0f, 1.0f);
@@ -301,7 +314,16 @@ namespace Barotrauma
DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams, ref _damagedTexturePath), sourceRectScale: sourceRectScale);
break;
case "conditionalsprite":
var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null, ref _texturePath), sourceRectScale: sourceRectScale);
string conditionalSpritePath = string.Empty;
GetSpritePath(subElement.GetChildElement("sprite") ?? subElement.GetChildElement("deformablesprite") ?? subElement, null, ref conditionalSpritePath);
if (conditionalSpritePath.IsNullOrEmpty())
{
DebugConsole.ThrowError($"Failed to find a sprite path in the conditional sprite defined in {character.SpeciesName}, limb {type}.",
contentPackage: subElement.ContentPackage);
}
var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(),
file: conditionalSpritePath,
sourceRectScale: sourceRectScale);
ConditionalSprites.Add(conditionalSprite);
if (conditionalSprite.DeformableSprite != null)
{
@@ -475,13 +497,24 @@ namespace Barotrauma
private string _damagedTexturePath;
private string GetSpritePath(ContentXElement element, SpriteParams spriteParams, ref string path)
{
if (path == null)
if (path.IsNullOrEmpty())
{
if (spriteParams != null)
{
//1. check if the variant file redefines the texture
ContentPath texturePath = character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage);
//2. check if the base prefab defines the texture
ContentPath texturePath;
//1. check if the limb defines the texture directly
var definedTexturePath = element?.GetAttributeContentPath("texture");
if (!definedTexturePath.IsNullOrEmpty())
{
texturePath = definedTexturePath;
}
else
{
//2. check if the character file defines the texture directly
texturePath = character.Params.VariantFile?.GetRootExcludingOverride()?.GetAttributeContentPath("texture", character.Prefab.ContentPackage);
}
//3. check if the base prefab defines the texture
if (texturePath.IsNullOrEmpty() && !character.Prefab.VariantOf.IsEmpty)
{
Identifier speciesName = character.GetBaseCharacterSpeciesName();
@@ -491,7 +524,7 @@ namespace Barotrauma
texturePath = parentRagdollParams.OriginalElement?.GetAttributeContentPath("texture");
}
//3. "default case", get the texture from this character's XML
//4. "default case", get the texture from this character's XML
texturePath ??= ContentPath.FromRaw(spriteParams.Element.ContentPackage ?? character.Prefab.ContentPackage, spriteParams.GetTexturePath());
path = GetSpritePath(texturePath);
}
@@ -749,9 +782,29 @@ namespace Barotrauma
float herpesStrength = character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.SpaceHerpesType);
bool hideLimb = Hide ||
OtherWearables.Any(w => w.HideLimb) ||
WearingItems.Any(w => w.HideLimb);
bool hideLimb = ShouldHideLimb(this);
if (!hideLimb && Params.InheritHiding != LimbType.None)
{
if (character.AnimController.GetLimb(Params.InheritHiding) is Limb otherLimb)
{
hideLimb = ShouldHideLimb(otherLimb);
}
}
static bool ShouldHideLimb(Limb limb)
{
if (limb.Hide) { return true; }
// Performance-sensitive code -> implemented without Linq
foreach (var wearable in limb.OtherWearables)
{
if (wearable.HideLimb) { return true; }
}
foreach (var wearable in limb.WearingItems)
{
if (wearable.HideLimb) { return true; }
}
return false;
}
bool drawHuskSprite = HuskSprite != null && !wearableTypesToHide.Contains(WearableType.Husk);
@@ -771,11 +824,13 @@ namespace Barotrauma
{
if (ActiveDeformations.Any())
{
var deformation = SpriteDeformation.GetDeformation(ActiveDeformations, deformSprite.Size);
var deformation = SpriteDeformation.GetDeformation(ActiveDeformations, deformSprite.Size, flippedHorizontally: IsFlipped, false);
deformSprite.Deform(deformation);
if (LightSource != null && LightSource.DeformableLightSprite != null)
{
deformation = SpriteDeformation.GetDeformation(ActiveDeformations, deformSprite.Size, dir == Direction.Left);
//apparently inversing on the y-axis is only necessary for light sprites (see 345a65ca6)
//it's a mystery why this is the case, something to do with sprite flipping being handled differently in light rendering?
deformation = SpriteDeformation.GetDeformation(ActiveDeformations, deformSprite.Size, flippedHorizontally: IsFlipped, inverseY: dir == Direction.Left);
LightSource.DeformableLightSprite.Deform(deformation);
}
}
@@ -826,7 +881,7 @@ namespace Barotrauma
var defSprite = conditionalSprite.DeformableSprite;
if (ActiveDeformations.Any())
{
var deformation = SpriteDeformation.GetDeformation(ActiveDeformations, defSprite.Size);
var deformation = SpriteDeformation.GetDeformation(ActiveDeformations, defSprite.Size, flippedHorizontally: IsFlipped);
defSprite.Deform(deformation);
}
else
@@ -883,28 +938,28 @@ namespace Barotrauma
}
depthStep += step;
}
foreach (WearableSprite wearable in OtherWearables)
if (!hideLimb)
{
if (wearable.Type == WearableType.Husk) { continue; }
if (wearableTypesToHide.Contains(wearable.Type))
foreach (WearableSprite wearable in OtherWearables)
{
if (wearable.Type == WearableType.Hair)
if (wearable.Type == WearableType.Husk) { continue; }
if (wearableTypesToHide.Contains(wearable.Type))
{
if (HairWithHatSprite != null && !hideLimb)
// Draws the short hair
if (wearable.Type == WearableType.Hair)
{
DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect);
depthStep += step;
continue;
if (HairWithHatSprite != null)
{
DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect);
depthStep += step;
}
}
}
else
{
continue;
}
DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect);
//if there are multiple sprites on this limb, make the successive ones be drawn in front
depthStep += step;
}
DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect);
//if there are multiple sprites on this limb, make the successive ones be drawn in front
depthStep += step;
}
}
foreach (WearableSprite wearable in WearingItems)
@@ -952,8 +1007,8 @@ namespace Barotrauma
var ca = (float)Math.Cos(-body.Rotation);
var sa = (float)Math.Sin(-body.Rotation);
Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y);
decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c,
-body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect,
decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, decorativeSprite.Sprite.Origin,
-body.Rotation + rotation, decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect,
depth: activeSprite.Depth - depthStep);
depthStep += step;
}
@@ -963,7 +1018,7 @@ namespace Barotrauma
new Vector2(body.DrawPosition.X, -body.DrawPosition.Y),
colorWithoutTint * damageOverlayStrength, activeSprite.Origin,
-body.DrawRotation,
Scale, spriteEffect, activeSprite.Depth - depthStep * Math.Max(1, WearingItems.Count * 2)); // Multiply by 2 to get rid of z-fighting with some clothing combos
Scale * TextureScale, spriteEffect, activeSprite.Depth - depthStep * Math.Max(1, WearingItems.Count * 2)); // Multiply by 2 to get rid of z-fighting with some clothing combos
}
}

View File

@@ -86,7 +86,7 @@ namespace Barotrauma
var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame: !isEditor, showName: false, titleFont: GUIStyle.SubHeadingFont)
{
Readonly = CircuitBox.Locked
Readonly = CircuitBox.IsLocked()
};
fieldCount += componentEditor.Fields.Count;

View File

@@ -35,7 +35,7 @@ namespace Barotrauma
public List<CircuitBoxWireRenderer> VirtualWires = new();
public bool Locked => CircuitBox.Locked;
public bool Locked => CircuitBox.IsLocked();
public CircuitBoxUI(CircuitBox box)
{
@@ -786,6 +786,7 @@ namespace Barotrauma
if (wireOption.TryUnwrap(out var wire))
{
CircuitBox.RemoveWires(wire.IsSelected ? wireSelection : ImmutableArray.Create(wire));
return;
}
switch (nodeOption)

View File

@@ -169,9 +169,9 @@ namespace Barotrauma
return doc;
}
public void Save(string path)
public void Save(string path, bool catchUnauthorizedAccessExceptions = true)
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
Directory.CreateDirectory(Path.GetDirectoryName(path)!, catchUnauthorizedAccessExceptions);
ToXDocument().SaveSafe(path);
}
}

View File

@@ -265,7 +265,7 @@ namespace Barotrauma.Transition
subs = getFiles(oldSubsPath, "*.sub");
itemAssemblies = getFiles(oldItemAssembliesPath, "*.xml");
string[] allOldMods = Directory.GetDirectories(oldModsPath, "*", System.IO.SearchOption.TopDirectoryOnly);
string[] allOldMods = Directory.GetDirectories(oldModsPath, "*");
var publishedItems = await SteamManager.Workshop.GetPublishedItems();
foreach (var modDir in allOldMods)

View File

@@ -588,6 +588,39 @@ namespace Barotrauma
GameMain.CharacterEditorScreen.Select();
}));
commands.Add(new Command("settainted", "settainted [true/false]: Sets tainted effect on hovered genetic material.",
onExecute: (string[] args) =>
{
if (Character.Controlled == null)
{
NewMessage("No controlled character!", Color.Red);
return;
}
Item focusedItem = Character.Controlled?.FocusedItem ?? Inventory.SelectedSlot?.Item;
if (focusedItem == null)
{
NewMessage("No focused item, hover on something!", Color.Red);
return;
}
var geneticMaterial = focusedItem.GetComponent<GeneticMaterial>();
if (geneticMaterial == null)
{
NewMessage("Not hovering on a genetic material!", Color.Red);
return;
}
else
{
bool newValue = args.None(arg => string.Equals(arg, "false", StringComparison.InvariantCultureIgnoreCase));
geneticMaterial.SetTainted(newValue);
NewMessage($"Set tainted to {newValue} for {focusedItem.Name}", Color.Yellow);
}
}, isCheat: true));
commands.Add(new Command("quickstart", "Starts a singleplayer sandbox", (string[] args) =>
{
if (Screen.Selected != GameMain.MainMenuScreen)
@@ -626,6 +659,113 @@ namespace Barotrauma
}, getValidArgs: () => new[] { SubmarineInfo.SavedSubmarines.Select(s => s.Name).Distinct().OrderBy(s => s).ToArray() }));
commands.Add(new Command("forcewreck", "forcewreck [wreckname] (optional, ThalamusSpawn)[Random/Forced/Disabled]: When generating levels, ensures a specific wreck is generated. Second optional parameter to control thalamus spawning.", (string[] args) =>
{
if (args.Length > 0)
{
var submarineFile = GetSubmarineFile<WreckFile>(args[0]);
if (submarineFile != null)
{
var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(i => i.FilePath == submarineFile.Path.Value);
if (matchingSub != null)
{
NewMessage($"Setting ForceWreck to: {matchingSub.Name}, {submarineFile.Path}", color: Color.Yellow);
LevelData.ConsoleForceWreck = matchingSub;
}
}
else
{
NewMessage($"Can't find: {args[0]}", color: Color.Red);
}
}
if (args.Length > 1)
{
string forceThalamusArg = args[1];
if (Enum.TryParse(forceThalamusArg, ignoreCase: true, out LevelData.ThalamusSpawn result))
{
NewMessage($"Setting ThalamusSpawn to: {result}", color: Color.Yellow);
LevelData.ForceThalamus = result;
}
else
{
NewMessage($"Can't parse argument: {forceThalamusArg}", color: Color.Red);
}
}
else
{
NewMessage($"Setting ThalamusSpawn to: {LevelData.ThalamusSpawn.Random}", color: Color.Yellow);
LevelData.ForceThalamus = LevelData.ThalamusSpawn.Random;
}
},
() =>
{
return new string[][]
{
ListSubmarineFileNames<WreckFile>(),
new string[] { LevelData.ThalamusSpawn.Random.ToString(), LevelData.ThalamusSpawn.Forced.ToString(), LevelData.ThalamusSpawn.Disabled.ToString() }
};
}, isCheat: true));
commands.Add(new Command("forcebeaconstation|forcebeacon", "forcebeaconstation [station name]: When generating levels, ensures a specific beacon station is generated.", (string[] args) =>
{
if (args.Length > 0)
{
var submarineFile = GetSubmarineFile<BeaconStationFile>(args[0]);
if (submarineFile != null)
{
var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(i => i.FilePath == submarineFile.Path.Value);
if (matchingSub != null)
{
NewMessage($"Setting ForceBeaconStation to: {matchingSub.Name}, {submarineFile.Path}", color: Color.Yellow);
LevelData.ConsoleForceBeaconStation = matchingSub;
}
}
else
{
NewMessage($"Can't find: {args[0]}", color: Color.Red);
}
}
},
() =>
{
return new string[][]
{
ListSubmarineFileNames<BeaconStationFile>()
};
}, isCheat: true));
commands.Add(new Command("reloadcontentfile", "reloadcontentfile [filepath]: Reloads a specific content xml file during runtime.", (string[] args) =>
{
if (args.Length > 0)
{
string pathArgument = args[0];
var contentFile = GetContentFile(pathArgument);
if (contentFile != null)
{
NewMessage($"Reloading content file: {pathArgument}", Color.Yellow);
contentFile.UnloadFile();
contentFile.LoadFile();
}
else
{
NewMessage($"Can't find {args[0]} to reload", color:Color.Red);
}
}
},
() =>
{
return new string[][]
{
ListContentFilePaths()
};
}, isCheat: true));
commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks networking debug logging.", (string[] args) =>
{
SteamManager.SetSteamworksNetworkingDebugLog(!SteamManager.NetworkingDebugLog);
@@ -783,6 +923,7 @@ namespace Barotrauma
AssignRelayToServer("spreadsheetexport", false);
#if DEBUG
AssignRelayToServer("listspamfilters", false);
AssignRelayToServer("showitemxml", false);
AssignRelayToServer("crash", false);
AssignRelayToServer("showballastflorasprite", false);
AssignRelayToServer("simulatedlatency", false);
@@ -858,14 +999,8 @@ namespace Barotrauma
TeleportCharacter(cursorWorldPos, Character.Controlled, args);
});
AssignOnExecute("spawn|spawncharacter", (string[] args) =>
{
SpawnCharacter(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), out string errorMsg);
if (!string.IsNullOrWhiteSpace(errorMsg))
{
ThrowError(errorMsg);
}
});
AssignOnExecute("spawn|spawncharacter", args => SpawnCharacter(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition)));
AssignOnExecute("spawnnpc", args => SpawnCharacter(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), true));
AssignOnExecute("los", (string[] args) =>
{
@@ -1924,7 +2059,7 @@ namespace Barotrauma
addIfMissing($"missionname.{missionId}".ToIdentifier(), language);
}
if (missionPrefab.Type == MissionType.Combat)
if (missionPrefab.Type == Tags.MissionTypeCombat)
{
addIfMissing($"MissionDescriptionNeutral.{missionId}".ToIdentifier(), language);
addIfMissing($"MissionDescription1.{missionId}".ToIdentifier(), language);
@@ -2360,7 +2495,47 @@ namespace Barotrauma
GameMain.SubEditorScreen.LoadSub(wreckedSubmarineInfo);
}));
commands.Add(new Command("showitemxml", "showitemxml [item]: Shows the XML configuration of an item in the console and copies it to the clipboard. Useful for debugging variants that partially override the XML of the base item for example.", (string[] args) =>
{
if (args.Length == 0)
{
ThrowError("Please specify the name or identifier of the item.");
return;
}
string itemNameOrId = args[0].ToLowerInvariant();
ItemPrefab itemPrefab =
(MapEntityPrefab.FindByName(itemNameOrId) ??
MapEntityPrefab.FindByIdentifier(itemNameOrId.ToIdentifier())) as ItemPrefab;
if (itemPrefab == null)
{
ThrowError("Item \"{itemNameOrId}\" not found!");
return;
}
string xmlStr = itemPrefab.ConfigElement.Element.ToString();
NewMessage(xmlStr);
Clipboard.SetText(xmlStr);
}, getValidArgs: () =>
{
return new string[][]
{
GetItemNameOrIdParams().ToArray()
};
}));
#if DEBUG
commands.Add(new Command("unlockachievement", "unlockachievement [identifier]: Unlocks the specified achievement.", (string[] args) =>
{
if (args.Length < 1)
{
ThrowError("Please specify the achievement to unlock.");
return;
}
NewMessage($"Unlocked \"{args[0]}\".");
AchievementManager.UnlockAchievement(args[0].ToIdentifier());
}, isCheat: true));
commands.Add(new Command("deathprompt", "Shows the death prompt for testing purposes.", (string[] args) =>
{
DeathPrompt.Create(delay: 1.0f);
@@ -2665,7 +2840,7 @@ namespace Barotrauma
string[] lines;
try
{
lines = File.ReadAllLines(sourcePath);
lines = File.ReadAllLines(sourcePath, catchUnauthorizedAccessExceptions: false);
}
catch (Exception e)
{
@@ -2746,34 +2921,60 @@ namespace Barotrauma
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
}));
commands.Add(new Command("dumpeventtexts", "dumpeventtexts [filepath]: gets the texts from event files and and writes them into a file along with xml tags that can be used in translation files. If the filepath is omitted, the file is written to Content/Texts/EventTexts.txt", (string[] args) =>
commands.Add(new Command("dumpeventtexts", "dumpeventtexts [sourcepath] [destinationpath]: gets the texts from event files and writes them into a file along with xml tags that can be used in translation files. If the filepath arguments are omitted, all event files are gone through and written to Content/Texts/EventTexts.txt", (string[] args) =>
{
string filePath = args.Length > 0 ? args[0] : "Content/Texts/EventTexts.txt";
string sourcePath = args.Length > 0 ? Path.GetFullPath(args[0]) : string.Empty;
string destinationPath = args.Length > 1 ? args[1] : "Content/Texts/EventTexts.txt";
List<string> lines = new List<string>();
HashSet<XDocument> docs = new HashSet<XDocument>();
HashSet<string> textIds = new HashSet<string>();
Dictionary<string, string> existingTexts = new Dictionary<string, string>();
foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs())
{
if (eventPrefab is not TraitorEventPrefab) { continue; }
if (eventPrefab.Identifier.IsEmpty)
string dir = Path.GetDirectoryName(eventPrefab.FilePath.FullPath);
if (!sourcePath.IsNullOrEmpty() &&
Path.GetFullPath(eventPrefab.FilePath.FullPath) != sourcePath &&
Path.GetDirectoryName(eventPrefab.FilePath.FullPath) != sourcePath)
{
continue;
}
if (eventPrefab.Identifier.IsEmpty) { continue; }
docs.Add(eventPrefab.ConfigElement.Document);
getTextsFromElement(eventPrefab.ConfigElement, lines, eventPrefab.Identifier.Value);
NewMessage($"Collecting event texts from event \"{eventPrefab.Identifier}\"...", Color.Cyan);
}
if (lines.None())
{
if (sourcePath.IsNullOrEmpty())
{
ThrowError("Could not find any event texts. Have all the texts already been moved from the event files to the text files?");
}
else
{
ThrowError($"Could not find any event texts from \"{sourcePath}\". Are you sure the path is to a valid event xml file or a directory that contains event xml files?");
}
return;
}
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
File.WriteAllLines(filePath, lines);
try
{
ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
File.WriteAllLines(destinationPath, lines);
}
catch (Exception e)
{
ThrowError($"Failed to open the file \"{filePath}\".", e);
ThrowError($"Failed to write to the file \"{destinationPath}\".", e);
}
try
{
ToolBox.OpenFileWithShell(Path.GetFullPath(destinationPath));
NewMessage($"Wrote the event texts to a text file in \"{destinationPath}\".", Color.Cyan);
}
catch (Exception e)
{
ThrowError($"Failed to open the file \"{destinationPath}\".", e);
}
System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings
@@ -2783,10 +2984,12 @@ namespace Barotrauma
};
foreach (XDocument doc in docs)
{
using (var writer = XmlWriter.Create(new System.Uri(doc.BaseUri).LocalPath, settings))
string filePath = new System.Uri(doc.BaseUri).LocalPath;
using (var writer = XmlWriter.Create(filePath, settings))
{
doc.WriteTo(writer);
writer.Flush();
NewMessage($"Updated the event file \"{filePath}\".", Color.Cyan);
}
}
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
@@ -2805,14 +3008,10 @@ namespace Barotrauma
text = subTextElement?.GetAttributeString(textAttribute, null);
textElement = subTextElement;
}
if (text == null)
{
AddWarning("Failed to find text from the element " + element.ToString());
}
}
string textId = $"EventText.{parentName}";
if (!string.IsNullOrEmpty(text) && !text.Contains("EventText.", StringComparison.OrdinalIgnoreCase))
if (!string.IsNullOrEmpty(text) && !text.StartsWith("EventText.", StringComparison.OrdinalIgnoreCase) && !text.StartsWith("Tutorial.", StringComparison.OrdinalIgnoreCase))
{
if (existingTexts.TryGetValue(text, out string existingTextId))
{
@@ -2982,9 +3181,18 @@ namespace Barotrauma
}));
#if DEBUG
commands.Add(new Command("playovervc", "Plays a sound over voice chat.", (args) =>
{
VoipCapture.Instance?.SetOverrideSound(args.Length > 0 ? args[0] : null);
}));
commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) =>
{
if (args.Length != 1) { return; }
if (args.Length != 1)
{
ThrowError("Please specify a language to check.");
return;
}
TextManager.CheckForDuplicates(args[0].ToIdentifier().ToLanguageIdentifier());
}));
@@ -3443,9 +3651,7 @@ namespace Barotrauma
}
RagdollParams ragdollParams = character.AnimController.RagdollParams;
ragdollParams.LimbScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE);
var pos = character.WorldPosition;
character.AnimController.Recreate();
character.TeleportTo(pos);
character.AnimController.RecreateAndRespawn();
}, isCheat: true));
commands.Add(new Command("jointscale", "Define the jointscale for the controlled character. Provide id or name if you want to target another character. Note: the changes are not saved!", (string[] args) =>
@@ -3468,9 +3674,7 @@ namespace Barotrauma
}
RagdollParams ragdollParams = character.AnimController.RagdollParams;
ragdollParams.JointScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE);
var pos = character.WorldPosition;
character.AnimController.Recreate();
character.TeleportTo(pos);
character.AnimController.RecreateAndRespawn();
}, isCheat: true));
commands.Add(new Command("ragdollscale", "Rescale the ragdoll of the controlled character. Provide id or name if you want to target another character. Note: the changes are not saved!", (string[] args) =>
@@ -3494,9 +3698,7 @@ namespace Barotrauma
RagdollParams ragdollParams = character.AnimController.RagdollParams;
ragdollParams.LimbScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE);
ragdollParams.JointScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE);
var pos = character.WorldPosition;
character.AnimController.Recreate();
character.TeleportTo(pos);
character.AnimController.RecreateAndRespawn();
}, isCheat: true));
commands.Add(new Command("recreateragdoll", "Recreate the ragdoll of the controlled character. Provide id or name if you want to target another character.", (string[] args) =>
@@ -3507,21 +3709,43 @@ namespace Barotrauma
ThrowError("Not controlling any character!");
return;
}
var pos = character.WorldPosition;
character.AnimController.Recreate();
character.TeleportTo(pos);
}, isCheat: true));
character.AnimController.RecreateAndRespawn();
}, isCheat: true,
getValidArgs: () => new[] { GetSpawnedSpeciesNames() }));
commands.Add(new Command("resetragdoll", "Reset the ragdoll of the controlled character. Provide id or name if you want to target another character.", (string[] args) =>
commands.Add(new Command("resetragdoll", "Reset the ragdoll of the controlled character (and all of the same species). Provide species name if you want to target another character.", (string[] args) =>
{
var character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, true);
if (character == null)
IEnumerable<Character> characters;
if (args.Length == 0)
{
ThrowError("Not controlling any character!");
if (Character.Controlled == null)
{
ThrowError("Invalid species name! Press [TAB] to get valid options in this context.");
return;
}
// Reset all characters of the same species, because the same params affect them too.
characters = FindMatchingSpecies(Character.Controlled.SpeciesName.ToString());
}
else
{
characters = FindMatchingSpecies(args);
}
if (characters.None())
{
ThrowError("Invalid species name!");
return;
}
character.AnimController.ResetRagdoll(forceReload: true);
}, isCheat: true));
characters.ForEach(c => c.AnimController.ResetRagdoll());
foreach (Character character in characters)
{
// Variant scale multiplier doesn't work without recreating the ragdoll.
if (!character.VariantOf.IsEmpty)
{
character.AnimController.RecreateAndRespawn();
}
}
}, isCheat: true,
getValidArgs: () => new[] { GetSpawnedSpeciesNames() }));
commands.Add(new Command("loadanimation", "Loads an animation variation by name for the controlled character. The animation file has to be in the correct animations folder. Note: the changes are not saved!", (string[] args) =>
{
@@ -3546,6 +3770,29 @@ namespace Barotrauma
character.AnimController.TryLoadAnimation(animationType, Path.GetFileNameWithoutExtension(fileName), out _, throwErrors: true);
}, isCheat: true));
commands.Add(new Command("startlocalmptestsession", "startlocalmptestsession [(optional) number of clients, defaults to 2]: starts a new mp test session with multiple clients connected to local dedicated server", (string[] args) =>
{
// if we are not in main menu, exit out
if (Screen.Selected != GameMain.MainMenuScreen)
{
ThrowError("Must be in main menu to start.");
return;
}
// try to parse the number of clients
int numClients = 2;
if (args.Length > 0)
{
if (!int.TryParse(args[0], out numClients))
{
ThrowError("Failed to parse the number of clients.");
return;
}
}
StartLocalMPSession(numClients);
}));
commands.Add(new Command("reloadwearables", "Reloads the sprites of all limbs and wearable sprites (clothing) of the controlled character. Provide id or name if you want to target another character.", args =>
{
var character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, true);
@@ -3634,7 +3881,7 @@ namespace Barotrauma
ThrowError("Cannot use the flipx command while playing online.");
return;
}
if (Submarine.MainSub.SubBody != null) { Submarine.MainSub?.FlipX(); }
if (Submarine.MainSub?.SubBody != null) { Submarine.MainSub.FlipX(); }
}, isCheat: true));
commands.Add(new Command("head", "Load the head sprite and the wearables (hair etc). Required argument: head id. Optional arguments: hair index, beard index, moustache index, face attachment index.", args =>
@@ -3997,5 +4244,44 @@ namespace Barotrauma
componentCost += itemPrefab.DefaultPrice.Price;
}
}
public static void StartLocalMPSession(int numClients = 2)
{
try
{
if (Process.GetProcessesByName("DedicatedServer").Length == 0)
{
#if WINDOWS
Process.Start("DedicatedServer.exe", arguments: "-multiclienttestmode");
#else
Process.Start("./DedicatedServer", arguments: "-multiclienttestmode");
#endif
System.Threading.Thread.Sleep(1000);
}
GameMain.Client = new GameClient("client1",
new LidgrenEndpoint(System.Net.IPAddress.Loopback, NetConfig.DefaultPort), "localhost", Option<int>.None());
numClients = MathHelper.Clamp(numClients, 1, 4);
if (numClients > 1)
{
for (int i = 2; i <= numClients; i++)
{
System.Threading.Thread.Sleep(1000);
#if WINDOWS
Process.Start("Barotrauma.exe", arguments: "-connect server localhost -username client" + i);
#else
Process.Start("./Barotrauma", arguments: "-connect server localhost -username client" + i);
#endif
}
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to start the local MP test session", e);
}
}
}
}

View File

@@ -36,6 +36,7 @@ namespace Barotrauma
{
return
lastActiveAction != null &&
!lastActiveAction.ParentEvent.IsFinished &&
lastActiveAction.ParentEvent != ParentEvent &&
Timing.TotalTime < lastActiveAction.lastActiveTime + duration;
}
@@ -101,6 +102,7 @@ namespace Barotrauma
conversationList.BarScroll = (prevSize - conversationList.Content.Rect.Height) / (conversationList.TotalSize - conversationList.Content.Rect.Height);
conversationList.ScrollToEnd(duration: 0.5f);
lastMessageBox.SetBackgroundIcon(eventSprite);
MarkMessageBoxAsLastAction(lastMessageBox);
return;
}
}
@@ -123,16 +125,7 @@ namespace Barotrauma
messageBox.AutoClose = false;
GUIStyle.Apply(messageBox.InnerFrame, "DialogBox");
if (actionInstance != null)
{
lastActiveAction = actionInstance;
actionInstance.lastActiveTime = Timing.TotalTime;
actionInstance.dialogBox = messageBox;
}
else
{
messageBox.UserData = new Pair<string, UInt16>("ConversationAction", actionId.Value);
}
MarkMessageBoxAsLastAction(messageBox);
int padding = GUI.IntScale(16);
@@ -155,6 +148,20 @@ namespace Barotrauma
};
shadow.SetAsFirstChild();
void MarkMessageBoxAsLastAction(GUIMessageBox messageBox)
{
if (actionInstance != null)
{
lastActiveAction = actionInstance;
actionInstance.lastActiveTime = Timing.TotalTime;
actionInstance.dialogBox = messageBox;
}
else
{
messageBox.UserData = new Pair<string, UInt16>("ConversationAction", actionId.Value);
}
}
static void RecalculateLastMessage(GUIListBox conversationList, bool append)
{
if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement)

View File

@@ -48,6 +48,7 @@ partial class EventLog
textContent,
difficultyIconCount,
icon, GUIStyle.Red,
difficultyTooltipText: null,
out GUIImage missionIcon);
if (traitorResults != null &&

View File

@@ -549,6 +549,33 @@ namespace Barotrauma
}
public void ClientRead(IReadMessage msg)
{
if (GameMain.GameSession.IsRunning && !GameMain.Instance.LoadingScreenOpen)
{
ClientApplyNetworkMessage(msg);
}
else
{
//if the game session is not currently running (round still loading),
//we need to wait because the entities the status effect / conversation / etc targets may not exist yet
CoroutineManager.StartCoroutine(ApplyNetworkMessageWhenRoundLoaded(msg));
}
}
public IEnumerable<CoroutineStatus> ApplyNetworkMessageWhenRoundLoaded(IReadMessage msg)
{
while (GameMain.GameSession is { IsRunning: false } || GameMain.Instance.LoadingScreenOpen)
{
yield return new WaitForSeconds(1.0f);
}
if (GameMain.GameSession != null && GameMain.Client != null)
{
ClientApplyNetworkMessage(msg);
}
yield return CoroutineStatus.Success;
}
public void ClientApplyNetworkMessage(IReadMessage msg)
{
NetworkEventType eventType = (NetworkEventType)msg.ReadByte();
switch (eventType)

View File

@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
@@ -21,7 +21,12 @@ namespace Barotrauma
}
}
public override bool DisplayAsCompleted => State > 0 && requireRescue.None();
public override bool DisplayAsCompleted =>
!DisplayAsFailed &&
State > 0 &&
//don't display as completed mid-round if there's NPCs to rescue (mission isn't guaranteed to complete yet)
requireRescue.None();
public override bool DisplayAsFailed => State == HostagesKilledState;
public override void ClientReadInitial(IReadMessage msg)
@@ -47,7 +52,7 @@ namespace Barotrauma
#if CLIENT
if (allowOrderingRescuees)
{
GameMain.GameSession.CrewManager.AddCharacterToCrewList(character);
GameMain.GameSession.CrewManager?.AddCharacterToCrewList(character);
}
#endif
}
@@ -59,10 +64,13 @@ namespace Barotrauma
if (character.Submarine != null && character.AIController is EnemyAIController enemyAi)
{
enemyAi.UnattackableSubmarines.Add(character.Submarine);
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
foreach (Submarine sub in Submarine.MainSub.DockedTo)
if (Submarine.MainSub != null)
{
enemyAi.UnattackableSubmarines.Add(sub);
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
foreach (Submarine sub in Submarine.MainSub.DockedTo)
{
enemyAi.UnattackableSubmarines.Add(sub);
}
}
}
}

View File

@@ -1,7 +1,17 @@
namespace Barotrauma
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
namespace Barotrauma
{
partial class CombatMission : Mission
{
private readonly Dictionary<byte, int> clientKills = new Dictionary<byte, int>();
private readonly Dictionary<byte, int> clientDeaths = new Dictionary<byte, int>();
private readonly Dictionary<ushort, int> botKills = new Dictionary<ushort, int>();
private readonly Dictionary<ushort, int> botDeaths = new Dictionary<ushort, int>();
public override LocalizedString Description
{
get
@@ -21,5 +31,93 @@
public override bool DisplayAsCompleted => false;
public override bool DisplayAsFailed => false;
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (targetSubmarine == null)
{
yield break;
}
else
{
yield return (targetSubmarineSonarLabel is { Loaded: true } ? targetSubmarineSonarLabel : targetSubmarine.Info.DisplayName, targetSubmarine.WorldPosition);
}
}
}
public static Color GetTeamColor(CharacterTeamType teamID)
{
if (teamID == CharacterTeamType.Team1)
{
return GUIStyle.GetComponentStyle("CoalitionIcon")?.Color ?? GUIStyle.Blue;
}
else if (teamID == CharacterTeamType.Team2)
{
return GUIStyle.GetComponentStyle("SeparatistIcon")?.Color ?? GUIStyle.Orange;
}
return Color.White;
}
public int GetClientKillCount(Client client)
{
if (clientKills.TryGetValue(client.SessionId, out int kills))
{
return kills;
}
return 0;
}
public int GetClientDeathCount(Client client)
{
if (clientDeaths.TryGetValue(client.SessionId, out int deaths))
{
return deaths;
}
return 0;
}
public int GetBotKillCount(CharacterInfo botInfo)
{
if (botKills.TryGetValue(botInfo.ID, out int kills))
{
return kills;
}
return 0;
}
public int GetBotDeathCount(CharacterInfo botInfo)
{
if (botDeaths.TryGetValue(botInfo.ID, out int deaths))
{
return deaths;
}
return 0;
}
public override void ClientRead(IReadMessage msg)
{
base.ClientRead(msg);
Scores[0] = msg.ReadUInt16();
Scores[1] = msg.ReadUInt16();
uint clientCount = msg.ReadVariableUInt32();
for (int i = 0; i < clientCount; i++)
{
byte clientId = msg.ReadByte();
clientDeaths[clientId] = (int)msg.ReadVariableUInt32();
clientKills[clientId] = (int)msg.ReadVariableUInt32();
}
uint botCount = msg.ReadVariableUInt32();
for (int i = 0; i < botCount; i++)
{
ushort botId = msg.ReadUInt16();
botDeaths[botId] = (int)msg.ReadVariableUInt32();
botKills[botId] = (int)msg.ReadVariableUInt32();
}
}
}
}

View File

@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using static Barotrauma.MissionPrefab;
namespace Barotrauma
{
@@ -61,6 +60,16 @@ namespace Barotrauma
return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖" + rewardText + "‖end‖"));
}
public RichString GetDifficultyToolTipText()
{
// 2 skulls give +10% XP, 3 skulls +20% XP and 4 skulls give +30% XP.
float xpBonusMultiplier = CalculateDifficultyXPMultiplier();
float xpBonusPercentage = (xpBonusMultiplier - 1f) * 100f;
int bonusRounded = (int)Math.Round(xpBonusPercentage);
LocalizedString tooltipText = TextManager.GetWithVariable(tag: "missiondifficultyxpbonustooltip", varName: "[bonus]", value: bonusRounded.ToString());
return RichString.Rich(tooltipText);
}
public RichString GetReputationRewardText()
{
List<LocalizedString> reputationRewardTexts = new List<LocalizedString>();
@@ -117,6 +126,7 @@ namespace Barotrauma
return string.Empty;
}
}
partial void DistributeExperienceToCrew(IEnumerable<Character> crew, int experienceGain)
{
foreach (Character character in crew)

View File

@@ -50,6 +50,7 @@ namespace Barotrauma
return hudIconColor ?? IconColor;
}
}
public Color ProgressBarColor { get; private set; }
private Sprite hudIcon;
private Color? hudIconColor;
@@ -90,6 +91,7 @@ namespace Barotrauma
}
this.portraits = portraits.ToImmutableArray();
overrideMusicOnState = overrideMusic.ToImmutableDictionary();
ProgressBarColor = element.GetAttributeColor(nameof(ProgressBarColor), GUIStyle.Blue);
}
public Identifier GetOverrideMusicType(int state)

View File

@@ -1,4 +1,6 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Networking;
using FarseerPhysics;
namespace Barotrauma
@@ -8,26 +10,51 @@ namespace Barotrauma
public override bool DisplayAsCompleted => false;
public override bool DisplayAsFailed => false;
private void TryShowPickedUpMessage() => HandleMessage(ref pickedUpMessage);
private void TryShowRetrievedMessage()
{
if (DetermineCompleted())
{
if (!allRetrievedMessage.IsNullOrEmpty()) { CreateMessageBox(string.Empty, allRetrievedMessage); }
//no need to show this again, clear it
allRetrievedMessage = string.Empty;
HandleMessage(ref allRetrievedMessage);
}
else
{
if (!partiallyRetrievedMessage.IsNullOrEmpty()) { CreateMessageBox(string.Empty, partiallyRetrievedMessage); }
//no need to show this again, clear it
partiallyRetrievedMessage = string.Empty;
HandleMessage(ref partiallyRetrievedMessage);
}
}
private void HandleMessage(ref LocalizedString message)
{
if (!message.IsNullOrEmpty()) { CreateMessageBox(string.Empty, message); }
//no need to show this again, clear it
message = string.Empty;
}
public override void ClientReadInitial(IReadMessage msg)
{
base.ClientReadInitial(msg);
byte characterCount = msg.ReadByte();
for (int i = 0; i < characterCount; i++)
{
Character character = Character.ReadSpawnData(msg);
characters.Add(character);
ushort itemCount = msg.ReadUInt16();
for (int j = 0; j < itemCount; j++)
{
Item.ReadSpawnData(msg);
}
}
if (characters.Contains(null))
{
throw new System.Exception("Error in SalvageMission.ClientReadInitial: character list contains null (mission: " + Prefab.Identifier + ")");
}
if (characters.Count != characterCount)
{
throw new System.Exception("Error in SalvageMission.ClientReadInitial: character count does not match the server count (" + characters + " != " + characters.Count + "mission: " + Prefab.Identifier + ")");
}
foreach (var target in targets)
{
bool targetFound = msg.ReadBoolean();
@@ -81,24 +108,37 @@ namespace Barotrauma
{
base.ClientRead(msg);
bool atLeastOneTargetWasRetrieved = false;
bool showPickedUpMsg = false;
int targetCount = msg.ReadByte();
for (int i = 0; i < targetCount; i++)
{
var state = (Target.RetrievalState)msg.ReadByte();
if (i < targets.Count)
{
bool wasRetrieved = targets[i].Retrieved;
Target target = targets[i];
bool wasRetrieved = target.Retrieved;
bool wasPickedUp = target.State == Target.RetrievalState.PickedUp;
targets[i].State = state;
if (!wasRetrieved && targets[i].Retrieved)
if (!wasRetrieved && target.Retrieved)
{
atLeastOneTargetWasRetrieved = true;
}
else if (!wasPickedUp && target.State == Target.RetrievalState.PickedUp)
{
showPickedUpMsg = true;
}
}
}
if (atLeastOneTargetWasRetrieved)
{
TryShowRetrievedMessage();
}
if (showPickedUpMsg)
{
TryShowPickedUpMessage();
}
}
public override IEnumerable<Entity> HudIconTargets => targets.Where(static t => !t.Retrieved && t.Item.GetRootInventoryOwner() is not Character { IsLocalPlayer: true }).Select(static t => t.Item);
}
}

View File

@@ -7,20 +7,7 @@ namespace Barotrauma
{
partial class ScanMission : Mission
{
public override IEnumerable<Entity> HudIconTargets
{
get
{
if (State == 0)
{
return scanTargets.Where(kvp => !kvp.Value).Select(kvp => kvp.Key);
}
else
{
return Enumerable.Empty<Entity>();
}
}
}
public override IEnumerable<Entity> HudIconTargets => scanTargets.Where(kvp => !kvp.Value).Select(kvp => kvp.Key);
public override bool DisplayAsCompleted => false;
public override bool DisplayAsFailed => false;
@@ -62,7 +49,7 @@ namespace Barotrauma
ushort id = msg.ReadUInt16();
bool scanned = msg.ReadBoolean();
Entity entity = Entity.FindEntityByID(id);
if (!(entity is WayPoint wayPoint))
if (entity is not WayPoint wayPoint)
{
string errorMsg = $"Failed to find a waypoint in ScanMission.ClientReadScanTargetStatus. Entity {id} was {(entity?.ToString() ?? null)}";
DebugConsole.ThrowError(errorMsg);

View File

@@ -420,7 +420,6 @@ namespace Barotrauma
}
}
// TODO: refactor this further
private void HandleNewLineAndAlignment(
string text,
in Vector2 advanceUnit,
@@ -435,23 +434,29 @@ namespace Barotrauma
out uint charIndex,
out bool shouldContinue)
{
if ((alignment.HasFlag(Alignment.CenterX) || alignment.HasFlag(Alignment.Right)) && (lineWidth < 0.0f || text[i] == '\n'))
if (lineWidth < 0.0f || text[i] == '\n')
{
int startIndex = lineWidth < 0.0f ? i : (i + 1);
lineWidth = 0.0f;
for (int j = startIndex; j < text.Length; j++)
// Use bitwise operations instead of HasFlag or HasAnyFlag to avoid boxing, as this is performance-sensitive code.
bool isHorizontallyCentered = (alignment & Alignment.CenterX) == Alignment.CenterX;
bool isAlignedToRight = (alignment & Alignment.Right) == Alignment.Right;
if (isHorizontallyCentered || isAlignedToRight)
{
if (text[j] == '\n') { break; }
uint chrIndex = text[j];
int startIndex = lineWidth < 0.0f ? i : (i + 1);
lineWidth = 0.0f;
for (int j = startIndex; j < text.Length; j++)
{
if (text[j] == '\n') { break; }
uint chrIndex = text[j];
var gd2 = GetGlyphData(chrIndex);
lineWidth += gd2.Advance;
var gd2 = GetGlyphData(chrIndex);
lineWidth += gd2.Advance;
}
currentLineOffset = -lineWidth * advanceUnit * scale.X;
if (isHorizontallyCentered) { currentLineOffset *= 0.5f; }
currentLineOffset.X = MathF.Round(currentLineOffset.X);
currentLineOffset.Y = MathF.Round(currentLineOffset.Y);
}
currentLineOffset = -lineWidth * advanceUnit * scale.X;
if (alignment.HasFlag(Alignment.CenterX)) { currentLineOffset *= 0.5f; }
currentLineOffset.X = MathF.Round(currentLineOffset.X);
currentLineOffset.Y = MathF.Round(currentLineOffset.Y);
}
if (text[i] == '\n')
{
@@ -493,7 +498,7 @@ namespace Barotrauma
int lineNum = 0;
Vector2 currentPos = position;
Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation));
Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2(MathF.Cos(rotation), MathF.Sin(rotation));
for (int i = 0; i < text.Length; i++)
{
HandleNewLineAndAlignment(text, advanceUnit, position, scale, alignment, i,
@@ -504,7 +509,7 @@ namespace Barotrauma
GlyphData gd = GetGlyphData(charIndex);
if (gd.TexIndex >= 0)
{
if (gd.TexIndex < 0 || gd.TexIndex >= textures.Count)
if (gd.TexIndex >= textures.Count)
{
throw new ArgumentOutOfRangeException($"Error while rendering text. Texture index was out of range. Text: {text}, char: {charIndex} index: {gd.TexIndex}, texture count: {textures.Count}");
}
@@ -542,6 +547,11 @@ namespace Barotrauma
DynamicRenderAtlas(graphicsDevice, text);
}
quadVertices[0].Color = color;
quadVertices[1].Color = color;
quadVertices[2].Color = color;
quadVertices[3].Color = color;
Vector2 currentPos = position;
for (int i = 0; i < text.Length; i++)
{
@@ -558,26 +568,33 @@ namespace Barotrauma
if (gd.TexIndex >= 0)
{
float halfCharHeight = gd.TexCoords.Height * 0.5f;
float slantStrength = 0.35f;
float topItalicOffset = italics ? ((halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f : 0.0f;
float bottomItalicOffset = italics ? ((-halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f : 0.0f;
const float slantStrength = 0.35f;
float topItalicOffset = 0.0f;
float bottomItalicOffset = 0.0f;
if (italics)
{
topItalicOffset = ((halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f;
bottomItalicOffset = ((-halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f;
}
Texture2D tex = textures[gd.TexIndex];
float left = (float)gd.TexCoords.Left / tex.Width;
float bottom = (float)gd.TexCoords.Bottom / tex.Height;
float top = (float)gd.TexCoords.Top / tex.Height;
float right = (float)gd.TexCoords.Right / tex.Width;
quadVertices[0].Position = new Vector3(currentPos + gd.DrawOffset + (bottomItalicOffset, gd.TexCoords.Height), 0.0f);
quadVertices[0].TextureCoordinate = ((float)gd.TexCoords.Left / tex.Width, (float)gd.TexCoords.Bottom / tex.Height);
quadVertices[0].Color = color;
quadVertices[0].TextureCoordinate = new Vector2(left, bottom);
quadVertices[1].Position = new Vector3(currentPos + gd.DrawOffset + (topItalicOffset, 0.0f), 0.0f);
quadVertices[1].TextureCoordinate = ((float)gd.TexCoords.Left / tex.Width, (float)gd.TexCoords.Top / tex.Height);
quadVertices[1].Color = color;
quadVertices[1].TextureCoordinate = new Vector2(left, top);
quadVertices[2].Position = new Vector3(currentPos + gd.DrawOffset + (gd.TexCoords.Width + bottomItalicOffset, gd.TexCoords.Height), 0.0f);
quadVertices[2].TextureCoordinate = ((float)gd.TexCoords.Right / tex.Width, (float)gd.TexCoords.Bottom / tex.Height);
quadVertices[2].Color = color;
quadVertices[2].TextureCoordinate = new Vector2(right, bottom);
quadVertices[3].Position = new Vector3(currentPos + gd.DrawOffset + (gd.TexCoords.Width + topItalicOffset, 0.0f), 0.0f);
quadVertices[3].TextureCoordinate = ((float)gd.TexCoords.Right / tex.Width, (float)gd.TexCoords.Top / tex.Height);
quadVertices[3].Color = color;
quadVertices[3].TextureCoordinate = new Vector2(right, top);
sb.Draw(tex, quadVertices, 0.0f);
}

View File

@@ -22,13 +22,36 @@ namespace Barotrauma
private bool _toggleOpen = true;
public bool ToggleOpen
{
get { return _toggleOpen; }
set
{
_toggleOpen = PreferChatBoxOpen = value;
if (value) { hideableElements.Visible = true; }
}
get => _toggleOpen;
set => SetToggleOpenState(value, setPreference: true);
}
public static ChatBox GetChatBox()
{
if (GameMain.GameSession?.GameMode is not GameMode gameMode) { return null; }
return gameMode.IsSinglePlayer ? GameMain.GameSession.CrewManager?.ChatBox : GameMain.Client?.ChatBox;
}
public static void AutoHideChatBox() => SetChatBoxOpen(false);
private void SetToggleOpenState(bool value, bool setPreference = true)
{
_toggleOpen = value;
if (setPreference)
{
PreferChatBoxOpen = value;
}
if (value) { hideableElements.Visible = true; }
}
public static void ResetChatBoxOpenState() => GetChatBox()?.ResetOpenState();
public void ResetOpenState() => SetOpen(PreferChatBoxOpen);
private static void SetChatBoxOpen(bool isOpen) => GetChatBox()?.SetOpen(isOpen);
private void SetOpen(bool value) => SetToggleOpenState(value, setPreference: false);
private float openState;
public static bool PreferChatBoxOpen = true;
@@ -199,7 +222,7 @@ namespace Barotrauma
if (channelMemPending)
{
int.TryParse(channelText.Text, out int newChannel);
radio.SetChannelMemory(index, newChannel);
SetChannelMemory(index, newChannel);
btn.ToolTip = TextManager.GetWithVariables("radiochannelpreset",
("[index]", index.ToString()),
("[channel]", radio.GetChannelMemory(index).ToString()));
@@ -330,7 +353,7 @@ namespace Barotrauma
};
showNewMessagesButton.Visible = false;
ToggleOpen = PreferChatBoxOpen = GameSettings.CurrentConfig.ChatOpen;
SetToggleOpenState(GameSettings.CurrentConfig.ChatOpen, setPreference: true);
}
public void Toggle()
@@ -797,6 +820,15 @@ namespace Barotrauma
}
}
private void SetChannelMemory(int index, int channel)
{
if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio))
{
radio.SetChannelMemory(index, channel);
radio.Item.CreateClientEvent(radio);
}
}
public void ApplySelectionInputs() => ApplySelectionInputs(InputBox, true, ChatKeyStates.GetChatKeyStates());
public struct ChatKeyStates

View File

@@ -11,13 +11,14 @@ internal class DeathPrompt
{
private static CoroutineHandle? createPromptCoroutine;
private GUIFrame? deathPromptFrame;
private GUIComponent? skillPanel;
private GUIComponent? newCharacterPanel;
private GUIComponent? takeOverBotPanel;
private GUIComponent? content;
public static GUIComponent? takeOverBotPanelFrame;
private static GUIComponent? takeOverBotPanelFrame;
/// <summary>
/// Private constructor, because these should only be created using the Show method
@@ -58,7 +59,7 @@ internal class DeathPrompt
const float FadeInDuration = 1.0f;
bool permadeath = GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath };
bool ironman = GameMain.NetworkMember is { ServerSettings: { RespawnMode: RespawnMode.Permadeath, IronmanMode: true } };
bool ironman = GameMain.NetworkMember is { ServerSettings.IronmanModeActive: true };
var background = new GUICustomComponent(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), onDraw: DrawBackground)
{
@@ -73,11 +74,11 @@ internal class DeathPrompt
foreground.FadeIn(wait: 0, duration: 5.0f);
foreground.Pulsate(startScale: Vector2.One, Vector2.One * 0.8f, duration: 25.0f);
var frame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.3f), background.RectTransform, Anchor.Center))
deathPromptFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.3f), background.RectTransform, Anchor.Center))
{
UserData = this
};
frame.FadeIn(wait: 0, duration: FadeInDuration);
deathPromptFrame.FadeIn(wait: 0, duration: FadeInDuration);
new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.1f), background.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.2f) }, string.Empty, font: GUIStyle.LargeFont, textAlignment: Alignment.TopCenter)
{
@@ -90,7 +91,7 @@ internal class DeathPrompt
}
};
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center))
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), deathPromptFrame.RectTransform, Anchor.Center))
{
Stretch = true,
RelativeSpacing = 0.05f
@@ -188,7 +189,7 @@ internal class DeathPrompt
{
if (takeOverBotPanel == null)
{
CreateTakeOverBotPanel(frame, this);
CreateTakeOverBotPanel(deathPromptFrame, this);
}
else
{
@@ -202,7 +203,7 @@ internal class DeathPrompt
}
else
{
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), buttonContainerRight.RectTransform), TextManager.Get("deathprompt.respawnnow"))
var respawnNowButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), buttonContainerRight.RectTransform), TextManager.Get("deathprompt.respawnnow"))
{
OnClicked = (btn, userdata) =>
{
@@ -211,7 +212,13 @@ internal class DeathPrompt
return true;
},
Enabled = GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.MidRound }
}.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true);
};
if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.BetweenRounds })
{
respawnNowButton.ToolTip = TextManager.Get("respawnnotavailable.respawnmode.betweenrounds");
}
respawnNowButton.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true);
}
//"info buttons" at the bottom
@@ -249,7 +256,7 @@ internal class DeathPrompt
{
if (skillPanel == null)
{
CreateSkillPanel(frame, GameMain.Client?.Character?.Info ?? GameMain.Client?.CharacterInfo);
CreateSkillPanel(deathPromptFrame, GameMain.Client?.Character?.Info ?? GameMain.Client?.CharacterInfo);
}
else
{
@@ -266,7 +273,7 @@ internal class DeathPrompt
{
if (newCharacterPanel == null)
{
CreateNewCharacterPanel(frame);
CreateNewCharacterPanel(deathPromptFrame);
}
else
{
@@ -279,15 +286,6 @@ internal class DeathPrompt
}
}
//TODO
/*new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), infoButtonContainer.RectTransform), "Respawn settings", style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
return true;
}
}.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true);*/
this.content = background;
}
@@ -384,6 +382,8 @@ internal class DeathPrompt
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(onYes: () =>
{
GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
GameMain.NetLobbyScreen.CampaignCharacterDiscarded = false;
frame.Parent?.RemoveChild(frame);
newCharacterPanel = null;
});
@@ -465,6 +465,11 @@ internal class DeathPrompt
{
if (botList.SelectedData is CharacterInfo selectedCharacter && GameMain.Client is GameClient client)
{
if (!GetAvailableBots().Contains(selectedCharacter)) // Someone may have taken over the bot while the list was open, etc
{
CreateTakeOverBotPanel(frame, deathPrompt); // Update
return true;
}
client.SendTakeOverBotRequest(selectedCharacter);
GUIMessageBox.MessageBoxes.Remove(frame.Parent);
deathPrompt?.Close();
@@ -484,15 +489,26 @@ internal class DeathPrompt
return frame;
}
public void UpdateBotList()
{
if (deathPromptFrame != null && takeOverBotPanelFrame != null)
{
CloseBotPanel();
CreateTakeOverBotPanel(deathPromptFrame, deathPrompt: this);
}
}
private static IEnumerable<CharacterInfo> GetAvailableBots()
{
if (GameMain.GameSession?.CrewManager is { } crewManager)
{
return crewManager.GetCharacterInfos().Where(c =>
/*either an alive bot */
c is { Character.IsBot: true, Character.IsDead: false } ||
/* or a newly hired bot that hasn't spawned yet */
(c.IsNewHire && c.Character == null));
return crewManager.GetCharacterInfos(includeReserveBench: true).Where(c =>
// a bot on reserve bench
c.IsOnReserveBench ||
// an alive bot
(c.Character != null && c.Character is { IsBot: true, IsDead: false }) ||
// a newly hired bot that hasn't spawned yet
(c.Character == null && c.IsNewHire));
}
else
{

View File

@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Text;
using Barotrauma.Extensions;
namespace Barotrauma
@@ -84,16 +83,31 @@ namespace Barotrauma
{
currentDirectory += "/";
}
fileSystemWatcher?.Dispose();
fileSystemWatcher = new System.IO.FileSystemWatcher(currentDirectory)
try
{
Filter = "*",
NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName
};
fileSystemWatcher.Created += OnFileSystemChanges;
fileSystemWatcher.Deleted += OnFileSystemChanges;
fileSystemWatcher.Renamed += OnFileSystemChanges;
fileSystemWatcher.EnableRaisingEvents = true;
fileSystemWatcher?.Dispose();
fileSystemWatcher = new System.IO.FileSystemWatcher(currentDirectory)
{
Filter = "*",
NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName
};
fileSystemWatcher.Created += OnFileSystemChanges;
fileSystemWatcher.Deleted += OnFileSystemChanges;
fileSystemWatcher.Renamed += OnFileSystemChanges;
fileSystemWatcher.EnableRaisingEvents = true;
}
catch (System.IO.FileNotFoundException exception)
{
DebugConsole.ThrowError("Failed to set the current directory, possibly due to insufficient access permissions.", exception);
}
catch (ArgumentException exception)
{
DebugConsole.ThrowError("Failed to set the current directory, possibly because it was deleted.", exception);
}
catch (Exception exception)
{
DebugConsole.ThrowError("Failed to set the current directory for an unknown reason.", exception);
}
RefreshFileList();
}
}
@@ -218,6 +232,14 @@ namespace Barotrauma
{
if (Directory.Exists(txt))
{
var attributes = System.IO.File.GetAttributes(txt);
if (attributes.HasAnyFlag(System.IO.FileAttributes.System) || attributes.HasAnyFlag(System.IO.FileAttributes.Hidden))
{
// System and hidden folders should be filtered out when populating the options, but the user can still write or copy-paste the path in the text field,
// which will throw a file not found exception when the file system watcher starts. Therefore, this extra check.
tb.Text = CurrentDirectory;
return false;
}
CurrentDirectory = txt;
return true;
}
@@ -354,20 +376,19 @@ namespace Barotrauma
var directories = Directory.EnumerateDirectories(currentDirectory, "*" + filterBox!.Text + "*");
foreach (var directory in directories)
{
string txt = directory;
if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); }
if (!txt.EndsWith("/")) { txt += "/"; }
//get directory info
DirectoryInfo dirInfo = new DirectoryInfo(directory);
try
{
//this will throw an exception if the directory can't be opened
Directory.GetDirectories(directory);
//this will intentionally throw an exception if the directory can't be opened
System.IO.Directory.GetDirectories(directory);
}
catch (UnauthorizedAccessException)
{
// Skip the folders that can't be accessed.
continue;
}
string txt = directory;
if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); }
if (!txt.EndsWith("/")) { txt += "/"; }
var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt)
{
UserData = ItemIsDirectory.Yes

View File

@@ -106,6 +106,27 @@ namespace Barotrauma
public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth;
public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y);
public static float RelativeVerticalAspectRatio => VerticalAspectRatio / (ReferenceResolution.Y / ReferenceResolution.X);
/// <summary>
/// Returns the difference of the current aspect ratio to the reference aspect ratio (16:9).
/// E.g. if the aspect ratio is 16:9, returns 0; if it's 4:3, returns 0.444; if the aspect ratio is 12:5, returns -0.623.
/// </summary>
public static float AspectRatioDifference
{
get
{
// ~ 1.777
float referenceAspectRatio = ReferenceResolution.X / ReferenceResolution.Y;
float aspectRatioDifference = referenceAspectRatio - HorizontalAspectRatio;
if (MathUtils.NearlyEqual(aspectRatioDifference, 0))
{
// Handle possible rounding errors, so that we can trust that this returns 0 when the aspect ratio matches the reference aspect ratio.
return 0;
}
return aspectRatioDifference;
}
}
/// <summary>
/// A horizontal scaling factor for low aspect ratios (small width relative to height)
/// </summary>
@@ -113,6 +134,8 @@ namespace Barotrauma
public static bool IsUltrawide => HorizontalAspectRatio > 2.3f;
public static bool IsHUDScaled => GameSettings.CurrentConfig.Graphics.HUDScale > 1 || GameSettings.CurrentConfig.Graphics.InventoryScale > 1;
public static int UIWidth
{
get
@@ -469,7 +492,7 @@ namespace Barotrauma
"Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
soundTextY += yStep;
for (int i = 0; i < SoundManager.SOURCE_COUNT; i++)
for (int i = 0; i < SoundManager.SourceCount; i++)
{
Color clr = Color.White;
string soundStr = i + ": ";
@@ -625,9 +648,13 @@ namespace Barotrauma
DrawMessages(spriteBatch, cam);
if (MouseOn != null && !MouseOn.ToolTip.IsNullOrWhiteSpace())
if (MouseOn != null)
{
MouseOn.DrawToolTip(spriteBatch);
if (!MouseOn.ToolTip.IsNullOrWhiteSpace())
{
MouseOn.DrawToolTip(spriteBatch);
}
MouseOn.OnDrawToolTip?.Invoke(MouseOn);
}
if (SubEditorScreen.IsSubEditor())
@@ -1546,7 +1573,7 @@ namespace Barotrauma
private static readonly VertexPositionColorTexture[] donutVerts = new VertexPositionColorTexture[DonutSegments * 4];
public static void DrawDonutSection(
SpriteBatch sb, Vector2 center, Range<float> radii, float sectionRad, Color clr, float depth = 0.0f)
SpriteBatch sb, Vector2 center, Range<float> radii, float sectionRad, Color clr, float depth = 0.0f, float rotationRad = 0.0f)
{
float getRadius(int vertexIndex)
=> (vertexIndex % 4) switch
@@ -1589,7 +1616,7 @@ namespace Barotrauma
for (int vertexIndex = 0; vertexIndex < maxDirectionIndex * 4; vertexIndex++)
{
donutVerts[vertexIndex].Color = clr;
donutVerts[vertexIndex].Position = new Vector3(center + getDirection(vertexIndex) * getRadius(vertexIndex), 0.0f);
donutVerts[vertexIndex].Position = new Vector3(center + Vector2.Transform(getDirection(vertexIndex) * getRadius(vertexIndex), Matrix.CreateRotationZ(rotationRad)), 0.0f);
}
sb.Draw(solidWhiteTexture, donutVerts, depth, count: maxDirectionIndex);
}
@@ -1856,9 +1883,16 @@ namespace Barotrauma
Vector2 pos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) - new Vector2(HUDLayoutSettings.Padding) - 2 * Scale * sheet.FrameSize.ToVector2();
sheet.Draw(spriteBatch, (int)Math.Floor(savingIndicatorSpriteIndex), pos, savingIndicatorColor, origin: Vector2.Zero, rotate: 0.0f, scale: new Vector2(Scale));
}
#endregion
#region Element creation
public static void DrawCapsule(SpriteBatch sb, Vector2 origin, float length, float radius, float rotation, Color clr, float depth = 0, float thickness = 1)
{
DrawDonutSection(sb, origin + Vector2.Transform(-new Vector2(length / 2, 0), Matrix.CreateRotationZ(rotation)), new Range<float>(radius - thickness / 2, radius + thickness / 2), MathHelper.Pi, clr, depth, rotation - MathHelper.Pi);
DrawRectangle(sb, origin, new Vector2(length, radius * 2), new Vector2(length / 2, radius), rotation, clr, depth, thickness);
DrawDonutSection(sb, origin + Vector2.Transform(new Vector2(length / 2, 0), Matrix.CreateRotationZ(rotation)), new Range<float>(radius - thickness / 2, radius + thickness / 2), MathHelper.Pi, clr, depth, rotation);
}
#endregion
#region Element creation
public static Texture2D CreateCircle(int radius, bool filled = false)
{
@@ -2488,7 +2522,12 @@ namespace Barotrauma
{
IgnoreLayoutGroups = true,
ToolTip = TextManager.Get("bugreportbutton") + $" (v{GameMain.Version})",
OnClicked = (btn, userdata) => { GameMain.Instance.ShowBugReporter(); return true; }
OnClicked = (btn, userdata) =>
{
if (PauseMenuOpen) { TogglePauseMenu(); }
GameMain.Instance.ShowBugReporter();
return true;
}
};
CreateButton("PauseMenuResume", buttonContainer, null);
@@ -2524,23 +2563,37 @@ namespace Barotrauma
GameMain.GameSession?.EndRound("");
});
}
else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound))
else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null)
{
bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsFriendlyOutpostLevel();
if (canSave)
//server owner (host) can't return to the lobby without ending the round for everyone
if (!GameMain.Client.IsServerOwner)
{
CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToServerLobbyVerification", action: () =>
{
GameMain.Client?.RequestRoundEnd(save: true);
});
CreateButton("ReturnToServerlobby", buttonContainer,
verificationTextTag: "PauseMenuReturnToServerLobbyVerificationSelf",
action: () =>
{
GameMain.Client?.EndRoundForSelf();
});
}
CreateButton(GameMain.GameSession.GameMode is CampaignMode ? "ReturnToServerlobby" : "EndRound", buttonContainer,
verificationTextTag: GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd",
action: () =>
if (GameMain.Client.HasPermission(ClientPermissions.ManageRound))
{
bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsFriendlyOutpostLevel();
if (canSave)
{
GameMain.Client?.RequestRoundEnd(save: false);
});
CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToServerLobbyVerification", action: () =>
{
GameMain.Client?.RequestEndRound(save: true);
}, color: GUIStyle.Red);
}
CreateButton("EndRound", buttonContainer,
verificationTextTag: GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd",
action: () =>
{
GameMain.Client?.RequestEndRound(save: false);
}, color: GUIStyle.Red);
}
}
}
@@ -2568,9 +2621,9 @@ namespace Barotrauma
}
void CreateButton(string textTag, GUIComponent parent, Action action, string verificationTextTag = null)
void CreateButton(string textTag, GUIComponent parent, Action action, string verificationTextTag = null, Color? color = null)
{
new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), TextManager.Get(textTag))
var button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), TextManager.Get(textTag))
{
OnClicked = (btn, userData) =>
{
@@ -2586,25 +2639,29 @@ namespace Barotrauma
return true;
}
};
if (color.HasValue)
{
button.Color = color.Value;
}
}
void CreateVerificationPrompt(string textTag, Action confirmAction)
}
public static void CreateVerificationPrompt(string textTag, Action confirmAction)
{
var msgBox = new GUIMessageBox("", TextManager.Get(textTag),
new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") })
{
var msgBox = new GUIMessageBox("", TextManager.Get(textTag),
new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") })
{
UserData = "verificationprompt",
DrawOnTop = true
};
msgBox.Buttons[0].OnClicked = (_, __) =>
{
PauseMenuOpen = false;
confirmAction?.Invoke();
return true;
};
msgBox.Buttons[0].OnClicked += msgBox.Close;
msgBox.Buttons[1].OnClicked += msgBox.Close;
}
UserData = "verificationprompt",
DrawOnTop = true
};
msgBox.Buttons[0].OnClicked = (_, __) =>
{
PauseMenuOpen = false;
confirmAction?.Invoke();
return true;
};
msgBox.Buttons[0].OnClicked += msgBox.Close;
msgBox.Buttons[1].OnClicked += msgBox.Close;
}
private static bool TogglePauseMenu(GUIButton button, object obj)
@@ -2690,12 +2747,6 @@ namespace Barotrauma
}
}
public static bool IsFourByThree()
{
float aspectRatio = HorizontalAspectRatio;
return aspectRatio > 1.3f && aspectRatio < 1.4f;
}
public static void SetSavingIndicatorState(bool enabled)
{
if (enabled)

View File

@@ -159,6 +159,34 @@ namespace Barotrauma
}
}
private GUIComponent holdOverlay;
private bool requireHold;
public bool RequireHold
{
get => requireHold;
set
{
requireHold = value;
if (value)
{
holdOverlay ??= new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), Frame.RectTransform, Anchor.CenterLeft), style: null)
{
Color = GUIStyle.Yellow * 0.33f,
CanBeFocused = false,
IgnoreLayoutGroups = true,
Visible = true
};
}
else if (holdOverlay != null)
{
holdOverlay.Visible = false;
}
}
}
public float HoldDurationSeconds { get; set; } = 5f;
private float holdTimer;
public bool Pulse { get; set; }
private float pulseTimer;
private float pulseExpand;
@@ -240,6 +268,11 @@ namespace Barotrauma
}
if (PlayerInput.PrimaryMouseButtonHeld())
{
if (RequireHold)
{
holdTimer += deltaTime;
}
if (OnPressed != null)
{
if (OnPressed())
@@ -254,25 +287,34 @@ namespace Barotrauma
}
else if (PlayerInput.PrimaryMouseButtonClicked())
{
if (PlaySoundOnSelect)
if (!RequireHold || holdTimer > HoldDurationSeconds)
{
SoundPlayer.PlayUISound(ClickSound);
}
if (OnClicked != null)
{
if (OnClicked(this, UserData))
if (PlaySoundOnSelect)
{
State = ComponentState.Selected;
SoundPlayer.PlayUISound(ClickSound);
}
if (OnClicked != null)
{
if (OnClicked(this, UserData))
{
State = ComponentState.Selected;
}
}
else
{
Selected = !Selected;
}
}
else
{
Selected = !Selected;
}
}
else
{
holdTimer = 0.0f;
}
}
else
{
holdTimer = 0.0f;
if (!ExternalHighlight)
{
State = Selected ? ComponentState.Selected : ComponentState.None;
@@ -283,6 +325,20 @@ namespace Barotrauma
}
}
if (RequireHold)
{
float width = MathHelper.Clamp(holdTimer / HoldDurationSeconds, 0f, 1f);
if (!MathUtils.NearlyEqual(width, holdOverlay.RectTransform.RelativeSize.X))
{
holdOverlay.RectTransform.RelativeSize = new Vector2(width, 1f);
}
holdOverlay.Color =
holdTimer >= HoldDurationSeconds
? Color.Green * 0.33f
: Color.Red * 0.33f;
}
foreach (GUIComponent child in Children)
{
child.State = State;

View File

@@ -9,6 +9,7 @@ using Barotrauma.IO;
using RestSharp;
using System.Net;
using Barotrauma.Steam;
using Steamworks;
namespace Barotrauma
{
@@ -158,6 +159,12 @@ namespace Barotrauma
public Action<GUIComponent> OnAddedToGUIUpdateList;
/// <summary>
/// Triggers when a tooltip should be draw on the component.
/// Note that the callback triggers even if the item has no tooltip (which can be useful for e.g. only contructing the tooltip when needed).
/// </summary>
public Action<GUIComponent> OnDrawToolTip;
public enum ComponentState { None, Hover, Pressed, Selected, HoverSelected };
protected Alignment alignment;
@@ -247,6 +254,57 @@ namespace Barotrauma
get { return new Vector2(Rect.Center.X, Rect.Center.Y); }
}
/// <summary>
/// Clamps the component's rect position to the specified area. Does not resize the component.
/// </summary>
/// <param name="clampArea">Area to contain the Rect of this component to</param>
public void ClampToArea(Rectangle clampArea)
{
Rectangle componentRect = Rect;
int x = componentRect.X;
int y = componentRect.Y;
// Adjust the X position
if (componentRect.Width <= clampArea.Width)
{
if (componentRect.Left < clampArea.Left)
{
x = clampArea.Left;
}
else if (componentRect.Right > clampArea.Right)
{
x = clampArea.Right - componentRect.Width;
}
}
else
{
// Component is wider than clamp area, osition it to overlap as much as possible
x = clampArea.Left - (componentRect.Width - clampArea.Width) / 2;
}
// Adjust the Y position
if (componentRect.Height <= clampArea.Height)
{
if (componentRect.Top < clampArea.Top)
{
y = clampArea.Top;
}
else if (componentRect.Bottom > clampArea.Bottom)
{
y = clampArea.Bottom - componentRect.Height;
}
}
else
{
// Component is taller than clamp area, osition it to overlap as much as possible
y = clampArea.Top - (componentRect.Height - clampArea.Height) / 2;
}
Point moveAmount = new Point(x - componentRect.X, y - componentRect.Y);
RectTransform.ScreenSpaceOffset += moveAmount;
}
protected Rectangle ClampRect(Rectangle r)
{
if (Parent is null) { return r; }
@@ -1087,6 +1145,8 @@ namespace Barotrauma
FromXML(subElement, component is GUIListBox listBox ? listBox.Content.RectTransform : component.RectTransform);
}
component.toolTip = element.GetAttributeString("tooltip", string.Empty);
if (element.GetAttributeBool("resizetofitchildren", false))
{
Vector2 relativeResizeScale = element.GetAttributeVector2("relativeresizescale", Vector2.One);
@@ -1129,7 +1189,8 @@ namespace Barotrauma
{
foreach (XAttribute attribute in element.Attributes())
{
switch (attribute.Name.ToString().ToLowerInvariant())
string conditionName = attribute.Name.ToString().ToLowerInvariant();
switch (conditionName)
{
case "language":
var languages = element.GetAttributeIdentifierArray(attribute.Name.ToString(), Array.Empty<Identifier>())
@@ -1171,6 +1232,20 @@ namespace Barotrauma
#endif
}
return false;
case "mingamelaunches":
if (int.TryParse(attribute.Value, out int minLaunches))
{
return SteamManager.GetStatInt(AchievementStat.GameLaunchCount) > minLaunches;
}
return false;
case "appsubscribed":
case "appnotsubscribed":
if (SteamManager.IsInitialized &&
int.TryParse(attribute.Value, out int appId))
{
return SteamApps.IsSubscribedToApp(appId) == (conditionName == "appsubscribed");
}
return false;
}
}

View File

@@ -9,8 +9,21 @@ namespace Barotrauma
{
public class GUIDropDown : GUIComponent, IKeyboardSubscriber
{
/// <param name="selected">The component that was selected from the dropdown.</param>
/// <param name="obj"><see cref="GUIComponent.UserData"/> of the component selected from the dropdown.</param>
public delegate bool OnSelectedHandler(GUIComponent selected, object obj = null);
/// <summary>
/// Triggers when some item is cliecked from the dropdown.
/// Note that <see cref="SelectedData"/> is not set yet when this callback triggers, and returning false from the callback disallows selecting it.
/// If you want to access the new value, use the obj argument.
/// </summary>
public OnSelectedHandler OnSelected;
/// <summary>
/// Triggers after an item has been selected from the dropdown, all validation has been done and the new value has been set.
/// </summary>
public OnSelectedHandler AfterSelected;
public OnSelectedHandler OnDropped;
private readonly GUIButton button;
@@ -166,7 +179,7 @@ namespace Barotrauma
public Vector4 Padding => button.TextBlock.Padding;
public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false, Alignment textAlignment = Alignment.CenterLeft) : base(style, rectT)
public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false, Alignment textAlignment = Alignment.CenterLeft, float listBoxScale = 1) : base(style, rectT)
{
text ??= LocalizedString.EmptyString;
@@ -185,13 +198,21 @@ namespace Barotrauma
Anchor listAnchor = dropAbove ? Anchor.TopCenter : Anchor.BottomCenter;
Pivot listPivot = dropAbove ? Pivot.BottomCenter : Pivot.TopCenter;
listBox = new GUIListBox(new RectTransform(new Point(Rect.Width, Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, listAnchor, listPivot)
listBox = new GUIListBox(new RectTransform(new Point((int)(Rect.Width * listBoxScale), Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, listAnchor, listPivot)
{ IsFixedSize = false }, style: null)
{
Enabled = !selectMultiple,
PlaySoundOnSelect = true,
};
if (!selectMultiple) { listBox.OnSelected = SelectItem; }
if (!selectMultiple)
{
listBox.AfterSelected = (component, obj) =>
{
SelectItem(component, obj);
AfterSelected?.Invoke(component, obj);
return true;
};
}
GUIStyle.Apply(listBox, "GUIListBox", this);
GUIStyle.Apply(listBox.ContentBackground, "GUIListBox", this);
@@ -199,6 +220,8 @@ namespace Barotrauma
{
icon = new GUIImage(new RectTransform(new Vector2(0.6f, 0.6f), button.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5, 0) }, null, scaleToFit: true);
icon.ApplyStyle(button.Style.ChildStyles["dropdownicon".ToIdentifier()]);
//move the text away from the icon
button.TextBlock.Padding += new Vector4(0, 0, icon.Rect.Width, 0);
}
currentHighestParent = FindHighestParent();
@@ -249,12 +272,12 @@ namespace Barotrauma
toolTip ??= "";
if (selectMultiple)
{
var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, style: "ListBoxElement", color: color)
var frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, style: "ListBoxElement", color: color)
{
UserData = userData,
ToolTip = toolTip
};
new GUITickBox(new RectTransform(new Vector2(1.0f, 0.8f), frame.RectTransform, anchor: Anchor.CenterLeft) { MaxSize = new Point(int.MaxValue, (int)(button.Rect.Height * 0.8f)) }, text)
var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.8f), frame.RectTransform, anchor: Anchor.CenterLeft) { MaxSize = new Point(int.MaxValue, (int)(button.Rect.Height * 0.8f)) }, text)
{
UserData = userData,
ToolTip = toolTip,
@@ -266,6 +289,11 @@ namespace Barotrauma
return false;
}
if (OnSelected != null && !OnSelected.Invoke(tb.Parent, tb.Parent.UserData))
{
return false;
}
List<LocalizedString> texts = new List<LocalizedString>();
selectedDataMultiple.Clear();
selectedIndexMultiple.Clear();
@@ -282,8 +310,7 @@ namespace Barotrauma
i++;
}
button.Text = LocalizedString.Join(", ", texts);
// TODO: The callback is called at least twice, remove this?
OnSelected?.Invoke(tb.Parent, tb.Parent.UserData);
AfterSelected?.Invoke(tb.Parent, SelectedData);
return true;
}
};
@@ -291,7 +318,7 @@ namespace Barotrauma
}
else
{
return new GUITextBlock(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, text, style: "ListBoxElement", color: color, textColor: textColor)
return new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, text, style: "ListBoxElement", color: color, textColor: textColor)
{
UserData = userData,
ToolTip = toolTip
@@ -328,9 +355,8 @@ namespace Barotrauma
}
button.Text = textBlock?.Text ?? "";
}
OnSelected?.Invoke(component, obj);
Dropped = false;
// TODO: OnSelected can be called multiple times and when it shouldn't be called -> turn into an event so that nobody else can call it.
OnSelected?.Invoke(component, component.UserData);
return true;
}
@@ -344,6 +370,7 @@ namespace Barotrauma
{
listBox.Select(userData);
}
AfterSelected?.Invoke(SelectedComponent, SelectedData);
}
public void Select(int index)
@@ -360,6 +387,7 @@ namespace Barotrauma
{
listBox.Select(index);
}
AfterSelected?.Invoke(this, SelectedData);
}
private bool wasOpened;

View File

@@ -14,8 +14,17 @@ namespace Barotrauma
protected List<GUIComponent> selected;
public delegate bool OnSelectedHandler(GUIComponent component, object obj);
/// <summary>
/// Triggers when some element is clicked on the listbox.
/// Note that <see cref="SelectedData"/> is not set yet when this callback triggers, and returning false from the callback disallows selecting it.
/// </summary>
public OnSelectedHandler OnSelected;
/// <summary>
/// Triggers after some element has been selected from the listbox.
/// </summary>
public OnSelectedHandler AfterSelected;
public delegate object CheckSelectedHandler();
public CheckSelectedHandler CheckSelected;
@@ -1021,7 +1030,7 @@ namespace Barotrauma
while (index < Content.CountChildren)
{
GUIComponent child = Content.GetChild(index);
if (child.Visible)
if (child.Visible && child.CanBeFocused)
{
Select(index, force, GetAutoScroll(!SmoothScroll && autoScroll == AutoScroll.Enabled), takeKeyBoardFocus, playSelectSound);
if (SmoothScroll)
@@ -1040,7 +1049,7 @@ namespace Barotrauma
while (index >= 0)
{
GUIComponent child = Content.GetChild(index);
if (child.Visible)
if (child.Visible && child.CanBeFocused)
{
Select(index, force, GetAutoScroll(!SmoothScroll && autoScroll == AutoScroll.Enabled), takeKeyBoardFocus, playSelectSound);
if (SmoothScroll)
@@ -1151,6 +1160,8 @@ namespace Barotrauma
{
SoundPlayer.PlayUISound(GUISoundType.Select);
}
AfterSelected?.Invoke(child, SelectedData);
}
public void Select(IEnumerable<GUIComponent> children)
@@ -1160,6 +1171,7 @@ namespace Barotrauma
selected.Clear();
selected.AddRange(children.Where(c => Content.Children.Contains(c)));
foreach (var child in selected) { OnSelected?.Invoke(child, child.UserData); }
AfterSelected?.Invoke(children.FirstOrDefault(), SelectedData);
}
public void Deselect()
@@ -1172,6 +1184,15 @@ namespace Barotrauma
selected.Clear();
}
public void DeselectElement(GUIComponent child)
{
if (child == null) { return; }
if (selected.Contains(child))
{
selected.Remove(child);
}
}
public void UpdateScrollBarSize()
{
scrollBarNeedsRecalculation = false;
@@ -1268,7 +1289,7 @@ namespace Barotrauma
ContentBackground.DrawManually(spriteBatch, alsoChildren: false);
Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
if (HideChildrenOutsideFrame)
if (HideChildrenOutsideFrame && Content.CountChildren > 0)
{
spriteBatch.End();
spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, Content.Rect);
@@ -1306,7 +1327,7 @@ namespace Barotrauma
GUI.DrawRectangle(spriteBatch, drawRect, Color.White * 0.5f, thickness: 2f);
}
if (HideChildrenOutsideFrame)
if (HideChildrenOutsideFrame && Content.CountChildren > 0)
{
spriteBatch.End();
spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;

View File

@@ -289,7 +289,7 @@ namespace Barotrauma
GUIStyle.Apply(Text, "", this);
Content.Recalculate();
Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize =
new Point(Text.Rect.Width, Text.Rect.Height);
new Point(Text.Rect.Width, Math.Min(Text.Rect.Height, GameMain.GraphicsHeight));
Text.RectTransform.IsFixedSize = true;
if (headerText.IsNullOrWhiteSpace())
{

View File

@@ -52,6 +52,16 @@ namespace Barotrauma
public readonly static GUISprite BrokenIcon = new GUISprite("BrokenIcon");
public readonly static GUISprite YouAreHereCircle = new GUISprite("YouAreHereCircle");
public readonly static GUISprite SubLocationIcon = new GUISprite("SubLocationIcon");
public readonly static GUISprite ShuttleIcon = new GUISprite("ShuttleIcon");
public readonly static GUISprite WreckIcon = new GUISprite("WreckIcon");
public readonly static GUISprite CaveIcon = new GUISprite("CaveIcon");
public readonly static GUISprite OutpostIcon = new GUISprite("OutpostIcon");
public readonly static GUISprite RuinIcon = new GUISprite("RuinIcon");
public readonly static GUISprite EnemyIcon = new GUISprite("EnemyIcon");
public readonly static GUISprite CorpseIcon = new GUISprite("CorpseIcon");
public readonly static GUISprite BeaconIcon = new GUISprite("BeaconIcon");
public readonly static GUISprite Radiation = new GUISprite("Radiation");
public readonly static GUISpriteSheet RadiationAnimSpriteSheet = new GUISpriteSheet("RadiationAnimSpriteSheet");

View File

@@ -918,5 +918,12 @@ namespace Barotrauma
DebugConsole.ThrowError($"GUITextBox: Invalid selection: ({exception})");
}
}
public void ResetDelegates()
{
OnKeyHit = null;
OnEnterPressed = null;
OnTextChanged = null;
}
}
}

View File

@@ -39,6 +39,8 @@ namespace Barotrauma
GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false } &&
GameMain.Client?.CharacterInfo is { PermanentlyDead: true };
private static bool ReserveBenchEnabled => GameMain.GameSession?.Campaign is MultiPlayerCampaign;
private bool hadPermissionToHire;
private static bool HasPermissionToHire => ReplacingPermanentlyDeadCharacter ?
GameMain.NetworkMember?.ServerSettings.ReplaceCostPercentage <= 0 || CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMoney) || CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires) :
@@ -277,13 +279,42 @@ namespace Barotrauma
}
else
{
PendingHires?.ForEach(ci => AddPendingHire(ci));
PendingHires?.ForEach(ci => AddPendingHire(ci, createNetworkMessage: false));
}
SetTotalHireCost();
}
UpdateCrew();
}
/// <summary>
/// This will simply update each of the HR view lists (hireables, pending hires, and crew) from the most up to date information.
/// It is a sane version of UpdateLocationView that won't break things even if used outside of whatever arbitrary conditions that one was made for.
/// </summary>
public void RefreshHRView()
{
if (campaign?.CurrentLocation is not Location currentLocation)
{
return;
}
if (characterPreviewFrame != null)
{
characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame);
characterPreviewFrame = null;
}
UpdateHireables(currentLocation);
if (pendingList != null)
{
pendingList.Content.ClearChildren();
PendingHires?.ForEach(ci => AddPendingHire(ci, checkCrewSizeLimit: false, createNetworkMessage: false)); // don't check limits here, just display the data as it is
SetTotalHireCost();
}
UpdateCrew();
}
public void UpdateHireables()
{
UpdateHireables(campaign?.CurrentLocation);
@@ -329,10 +360,11 @@ namespace Barotrauma
public void UpdateCrew()
{
crewList.Content.Children.ToList().ForEach(c => crewList.Content.RemoveChild(c));
foreach (CharacterInfo c in GameMain.GameSession.CrewManager.GetCharacterInfos())
foreach (CharacterInfo ci in GameMain.GameSession.CrewManager.GetCharacterInfos(includeReserveBench: true))
{
if (c == null || !((c.Character?.IsBot ?? true) || campaign is SinglePlayerCampaign)) { continue; }
CreateCharacterFrame(c, crewList);
// CrewManager is used to store info on all characters including players, but we only want bots in HR
if (ci.Character != null && (ci.Character.IsRemotePlayer || !ci.Character.IsBot)) { continue; }
CreateCharacterFrame(ci, crewList);
}
SortCharacters(crewList, SortingMethod.JobAsc);
crewList.UpdateScrollBarSize();
@@ -370,6 +402,10 @@ namespace Barotrauma
if (sortingMethod == SortingMethod.SkillDesc) { list.Content.RectTransform.ReverseChildren(); }
}
// Always apply this in the end to group by reserve bench status (does nothing if there are no reserve benched bots)
list.Content.RectTransform.SortChildren((x, y) =>
((InfoSkill)x.GUIComponent.UserData).CharacterInfo.BotStatus.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.BotStatus));
int? CompareReputationRequirement(GUIComponent c1, GUIComponent c2)
{
CharacterInfo info1 = ((InfoSkill)c1.UserData).CharacterInfo;
@@ -401,6 +437,8 @@ namespace Barotrauma
public GUIComponent CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox, bool hideSalary = false)
{
string characterName = listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name;
Skill skill = null;
Color? jobColor = null;
if (characterInfo.Job != null)
@@ -415,6 +453,7 @@ namespace Barotrauma
};
GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), frame.RectTransform, anchor: Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
AbsoluteSpacing = 1,
Stretch = true
};
@@ -428,12 +467,14 @@ namespace Barotrauma
GUILayoutGroup nameAndJobGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f - portraitWidth, 0.8f), mainGroup.RectTransform)) { CanBeFocused = false };
GUILayoutGroup nameGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { CanBeFocused = false };
GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameGroup.RectTransform),
listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name,
characterName,
textColor: jobColor, textAlignment: Alignment.BottomLeft)
{
CanBeFocused = false
};
nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width);
const float smallColumnWidth = 0.6f / 3;
const float skillColumnWidth = smallColumnWidth * 0.7f;
const float buttonWidth = 0.12f;
GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform),
characterInfo.Title ?? characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft)
@@ -449,33 +490,28 @@ namespace Barotrauma
}
}
var fullJobText = jobBlock.Text;
jobBlock.Text = ToolBox.LimitString(fullJobText, jobBlock.Font, jobBlock.Rect.Width);
if (jobBlock.Text != fullJobText)
{
jobBlock.ToolTip = fullJobText;
jobBlock.CanBeFocused = true;
}
float width = 0.6f / 3;
if (characterInfo.Job != null && skill != null)
{
GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(width, 0.6f), mainGroup.RectTransform), isHorizontal: true);
GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(skillColumnWidth, 0.6f), mainGroup.RectTransform), isHorizontal: true);
float iconWidth = (float)skillGroup.Rect.Height / skillGroup.Rect.Width;
new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 1.0f), skillGroup.RectTransform), ((int)skill.Level).ToString(),
textAlignment: Alignment.CenterRight)
{
Padding = Vector4.Zero,
CanBeFocused = false
};
GUIImage skillIcon = new GUIImage(new RectTransform(Vector2.One, skillGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), skill.Icon, scaleToFit: true)
{
CanBeFocused = false
};
if (jobColor.HasValue) { skillIcon.Color = jobColor.Value; }
new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 1.0f), skillGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.CenterLeft)
{
CanBeFocused = false
};
}
if (!hideSalary)
{
if (listBox != crewList)
{
new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform),
new GUITextBlock(new RectTransform(new Vector2(smallColumnWidth, 1.0f), mainGroup.RectTransform),
TextManager.FormatCurrency(ReplacingPermanentlyDeadCharacter ? campaign.NewCharacterCost(characterInfo) : HireManager.GetSalaryFor(characterInfo)),
textAlignment: Alignment.Center)
{
@@ -485,19 +521,24 @@ namespace Barotrauma
else
{
// Just a bit of padding to make list layouts similar
new GUIFrame(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), style: null) { CanBeFocused = false };
new GUIFrame(new RectTransform(new Vector2(smallColumnWidth, 1.0f), mainGroup.RectTransform), style: null) { CanBeFocused = false };
}
}
if (listBox == hireableList)
{
var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton")
var hireButton = new GUIButton(new RectTransform(new Vector2(buttonWidth, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton")
{
ToolTip = TextManager.Get("hirebutton"),
ToolTip = TextManager.Get(ReserveBenchEnabled ? "hirebutton.crew" : "hirebutton"),
ClickSound = GUISoundType.Cart,
UserData = characterInfo,
Enabled = CanHire(characterInfo) && !ReplacingPermanentlyDeadCharacter,
OnClicked = (b, o) => AddPendingHire(o as CharacterInfo)
OnClicked = (b, o) =>
{
var currentCharacterInfo = (CharacterInfo)o;
currentCharacterInfo.BotStatus = BotStatus.PendingHireToActiveService;
return AddPendingHire(currentCharacterInfo);
}
};
hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) =>
{
@@ -505,7 +546,7 @@ namespace Barotrauma
{
return;
}
if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize)
if (PendingHires.Count(ci => ci.BotStatus == BotStatus.PendingHireToActiveService) + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize)
{
if (btn.Enabled)
{
@@ -523,7 +564,7 @@ namespace Barotrauma
if (ReplacingPermanentlyDeadCharacter)
{
bool canHire = CanHire(characterInfo) && campaign.CanAffordNewCharacter(characterInfo);
var takeoverButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementTakeControlButton")
var takeoverButton = new GUIButton(new RectTransform(new Vector2(buttonWidth, 0.9f), mainGroup.RectTransform), style: "CrewManagementTakeControlButton")
{
ToolTip = canHire ? TextManager.Get("hireandtakecontrol") : TextManager.Get("hireandtakecontroldisabled"),
ClickSound = GUISoundType.ConfirmTransaction,
@@ -554,25 +595,90 @@ namespace Barotrauma
btn.Enabled = canHireCurrently;
};
}
if (ReserveBenchEnabled && !ReplacingPermanentlyDeadCharacter)
{
var hireToReserveBenchButton = new GUIButton(new RectTransform(new Vector2(buttonWidth, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddAsReserveButton")
{
ToolTip = TextManager.Get("hirebutton.reservebench"),
ClickSound = GUISoundType.Cart,
UserData = characterInfo,
Enabled = CanHire(characterInfo),
OnClicked = (b, o) =>
{
var currentCharacterInfo = (CharacterInfo)o;
currentCharacterInfo.BotStatus = BotStatus.PendingHireToReserveBench;
return AddPendingHire(currentCharacterInfo, checkCrewSizeLimit: false);
}
};
hireToReserveBenchButton.OnAddedToGUIUpdateList += (GUIComponent btn) =>
{
btn.Visible = ReserveBenchEnabled;
btn.Enabled = CanHire(characterInfo) && !ReplacingPermanentlyDeadCharacter;
};
}
}
else if (listBox == pendingList)
{
new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementRemoveButton")
if (ReserveBenchEnabled && !ReplacingPermanentlyDeadCharacter)
{
new GUIButton(new RectTransform(new Vector2(buttonWidth, 0.9f), mainGroup.RectTransform),
style: characterInfo.BotStatus == BotStatus.PendingHireToActiveService ? "CrewManagementReserveBenchButtonActive" : "CrewManagementReserveBenchButtonReserve")
{
UserData = characterInfo,
ToolTip = TextManager.Get(characterInfo.BotStatus == BotStatus.PendingHireToActiveService ? "ReserveBenchTogglePendingHire.Active" : "ReserveBenchTogglePendingHire.Reserve"),
Enabled = CanHire(characterInfo) && (characterInfo.BotStatus == BotStatus.PendingHireToActiveService || !ActiveServiceFull()), // note that this is a toggle
OnClicked = (btn, obj) =>
{
SelectCharacter(null, null, null);
var currentCharacterInfo = (CharacterInfo)obj;
GameMain.Client?.ToggleReserveBench(currentCharacterInfo, pendingHire: true);
return true;
}
};
}
new GUIButton(new RectTransform(new Vector2(buttonWidth, 0.9f), mainGroup.RectTransform), style: "CrewManagementRemoveButton")
{
ClickSound = GUISoundType.Cart,
UserData = characterInfo,
Enabled = CanHire(characterInfo),
Enabled = CanHire(characterInfo), // =just check user's rights
OnClicked = (b, o) => RemovePendingHire(o as CharacterInfo)
};
}
else if (listBox == crewList && campaign != null)
{
var currentCrew = GameMain.GameSession.CrewManager.GetCharacterInfos();
new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementFireButton")
if (ReserveBenchEnabled && !ReplacingPermanentlyDeadCharacter)
{
new GUIButton(new RectTransform(new Vector2(buttonWidth, 0.9f), mainGroup.RectTransform),
style: characterInfo.BotStatus == BotStatus.ActiveService ? "CrewManagementReserveBenchButtonActive" : "CrewManagementReserveBenchButtonReserve")
{
UserData = characterInfo,
ToolTip = TextManager.Get(characterInfo.BotStatus == BotStatus.ActiveService ? "ReserveBenchToggle.Active" : "ReserveBenchToggle.Reserve"),
Enabled = CanHire(characterInfo) && (characterInfo.BotStatus == BotStatus.ActiveService || !ActiveServiceFull()), // note that this is a toggle
OnClicked = (btn, obj) =>
{
SelectCharacter(null, null, null);
var currentCharacterInfo = (CharacterInfo)obj;
if (currentCharacterInfo.BotStatus == BotStatus.ActiveService && // switching to reserve bench
characterInfo.Character != null) // may not have a Character to remove if not spawned this round
{
GameMain.GameSession.CrewManager.RemoveCharacter(characterInfo.Character, removeInfo: true, resetCrewListIndex: true);
}
GameMain.Client?.ToggleReserveBench(currentCharacterInfo); // update changes to server
return true;
}
};
}
var cm = GameMain.GameSession.CrewManager;
// Can't fire if there's only one character in active service
var fireButtonEnabled = HasPermissionToHire && (characterInfo.IsOnReserveBench ||
(cm.GetCharacterInfos().Contains(characterInfo) && cm.GetCharacterInfos().Count() > 1));
new GUIButton(new RectTransform(new Vector2(buttonWidth, 0.9f), mainGroup.RectTransform), style: "CrewManagementFireButton")
{
UserData = characterInfo,
//can't fire if there's only one character in the crew
Enabled = currentCrew.Contains(characterInfo) && currentCrew.Count() > 1 && HasPermissionToHire,
Enabled = fireButtonEnabled,
OnClicked = (btn, obj) =>
{
var confirmDialog = new GUIMessageBox(
@@ -587,11 +693,25 @@ namespace Barotrauma
}
};
}
else
{
if (ReserveBenchEnabled && characterInfo.IsOnReserveBench) // Applies to unspecified listings like the death prompt and the bot list after permadeath
{
new GUIImage(new RectTransform(new Vector2(smallColumnWidth / 2, 0.6f), mainGroup.RectTransform), style: "CrewManagementReserveBenchIconReserve")
{
ToolTip = TextManager.Get("ReserveBenchStatus.Reserve.WillSpawn")
};
}
else
{
new GUILayoutGroup(new RectTransform(new Vector2(smallColumnWidth / 2, 0.6f), mainGroup.RectTransform)) { CanBeFocused = false };
}
}
if (listBox == pendingList || listBox == crewList)
{
nameBlock.RectTransform.Resize(new Point(nameBlock.Rect.Width - nameBlock.Rect.Height, nameBlock.Rect.Height));
nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width);
nameBlock.Text = ToolBox.LimitString(characterName, nameBlock.Font, nameBlock.Rect.Width);
nameBlock.RectTransform.Resize(new Point((int)(nameBlock.Padding.X + nameBlock.TextSize.X + nameBlock.Padding.Z), nameBlock.Rect.Height));
Point size = new Point((int)(0.7f * nameBlock.Rect.Height));
new GUIImage(new RectTransform(size, nameGroup.RectTransform), "EditIcon") { CanBeFocused = false };
@@ -605,6 +725,16 @@ namespace Barotrauma
};
}
//recalculate everything and truncate texts if needed
mainGroup.Recalculate();
nameBlock.Text = ToolBox.LimitString(characterName, nameBlock.Font, nameBlock.Rect.Width);
jobBlock.Text = ToolBox.LimitString(fullJobText, jobBlock.Font, jobBlock.Rect.Width);
if (jobBlock.Text != fullJobText)
{
jobBlock.ToolTip = fullJobText;
jobBlock.CanBeFocused = true;
}
bool CanHire(CharacterInfo thisCharacterInfo)
{
if (!HasPermissionToHire) { return false; }
@@ -614,6 +744,15 @@ namespace Barotrauma
return frame;
}
/// <summary>
/// Is there (going to be) no space left in active service?
/// </summary>
private bool ActiveServiceFull()
{
return (PendingHires.Count(ci => ci.BotStatus == BotStatus.PendingHireToActiveService) + campaign.CrewManager.GetCharacterInfos().Count())
>= CrewManager.MaxCrewSize;
}
private bool EnoughReputationToHire(CharacterInfo characterInfo)
{
if (characterInfo.MinReputationToHire.factionId != Identifier.Empty)
@@ -656,7 +795,7 @@ namespace Barotrauma
GUILayoutGroup infoLabelGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), infoGroup.RectTransform)) { Stretch = true };
GUILayoutGroup infoValueGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), infoGroup.RectTransform)) { Stretch = true };
float blockHeight = 1.0f / 4;
new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("name"));
new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("name"), textColor: GUIStyle.TextColorBright);
GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), "");
string name = listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name;
nameBlock.Text = ToolBox.LimitString(name, nameBlock.Font, nameBlock.Rect.Width);
@@ -664,17 +803,17 @@ namespace Barotrauma
if (characterInfo.HasSpecifierTags)
{
var menuCategoryVar = characterInfo.Prefab.MenuCategoryVar;
new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get(menuCategoryVar));
new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get(menuCategoryVar), textColor: GUIStyle.TextColorBright);
new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get(characterInfo.ReplaceVars($"[{menuCategoryVar}]")));
}
if (characterInfo.Job is Job job)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("tabmenu.job"));
new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("tabmenu.job"), textColor: GUIStyle.TextColorBright);
new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), job.Name);
}
if (characterInfo.PersonalityTrait is NPCPersonalityTrait trait)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("PersonalityTrait"));
new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("PersonalityTrait"), textColor: GUIStyle.TextColorBright);
new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), trait.DisplayName);
}
infoLabelGroup.Recalculate();
@@ -727,9 +866,9 @@ namespace Barotrauma
return true;
}
private bool AddPendingHire(CharacterInfo characterInfo, bool createNetworkMessage = true)
private bool AddPendingHire(CharacterInfo characterInfo, bool checkCrewSizeLimit = true, bool createNetworkMessage = true)
{
if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize)
if (checkCrewSizeLimit && characterInfo.BotStatus == BotStatus.PendingHireToActiveService && ActiveServiceFull())
{
return false;
}
@@ -792,7 +931,7 @@ namespace Barotrauma
List<CharacterInfo> nonDuplicateHires = new List<CharacterInfo>();
hires.ForEach(hireInfo =>
{
if (campaign.CrewManager.GetCharacterInfos().None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName()))
if (campaign.CrewManager.GetCharacterInfos(includeReserveBench: true).None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName()))
{
nonDuplicateHires.Add(hireInfo);
}
@@ -806,12 +945,21 @@ namespace Barotrauma
if (!campaign.CanAfford(total)) { return false; }
}
bool atLeastOneHired = false;
bool atLeastOneHiredToActiveDuty = false;
bool atLeastOneHiredToReserveBench = false;
foreach (CharacterInfo ci in nonDuplicateHires)
{
bool toReserveBench = ci.BotStatus == BotStatus.PendingHireToReserveBench;
if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci, takeMoney: takeMoney))
{
atLeastOneHired = true;
if (toReserveBench)
{
atLeastOneHiredToReserveBench = true;
}
else
{
atLeastOneHiredToActiveDuty = true;
}
}
else
{
@@ -819,15 +967,27 @@ namespace Barotrauma
}
}
if (atLeastOneHired)
if (atLeastOneHiredToActiveDuty || atLeastOneHiredToReserveBench)
{
UpdateLocationView(campaign.Map.CurrentLocation, true);
SelectCharacter(null, null, null);
if (createNotification)
{
LocalizedString msg = string.Empty;
if (atLeastOneHiredToActiveDuty)
{
msg += TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName);
}
if (atLeastOneHiredToReserveBench)
{
if (!msg.IsNullOrEmpty()) { msg += "\n\n"; }
msg += GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false } ?
TextManager.Get("crewhiredmessage.reservebench.permadeath") :
TextManager.Get( "crewhiredmessage.reservebench");
}
var dialog = new GUIMessageBox(
TextManager.Get("newcrewmembers"),
TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName),
TextManager.Get("newcrewmembers"), msg,
new LocalizedString[] { TextManager.Get("Ok") });
dialog.Buttons[0].OnClicked += dialog.Close;
}
@@ -1034,7 +1194,7 @@ namespace Barotrauma
}
}
public void SetPendingHires(List<UInt16> characterInfos, Location location)
public void SetPendingHires(List<UInt16> characterInfos, bool[] characterInfoReserveBenchStatuses, Location location, bool checkCrewSizeLimit)
{
List<CharacterInfo> oldHires = PendingHires.ToList();
foreach (CharacterInfo pendingHire in oldHires)
@@ -1042,18 +1202,25 @@ namespace Barotrauma
RemovePendingHire(pendingHire, createNetworkMessage: false);
}
PendingHires.Clear();
int i = 0;
foreach (UInt16 identifier in characterInfos)
{
CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.ID == identifier);
if (match != null)
{
AddPendingHire(match, createNetworkMessage: false);
match.BotStatus = characterInfoReserveBenchStatuses[i] ? BotStatus.PendingHireToReserveBench : BotStatus.PendingHireToActiveService;
AddPendingHire(match, checkCrewSizeLimit: checkCrewSizeLimit, createNetworkMessage: false);
if (!PendingHires.Contains(match))
{
DebugConsole.ThrowError("Failed to add a pending hire");
}
System.Diagnostics.Debug.Assert(PendingHires.Contains(match));
}
else
{
DebugConsole.ThrowError("Received a hire that doesn't exist.");
}
i++;
}
}
@@ -1064,7 +1231,7 @@ namespace Barotrauma
/// <param name="renameCharacter">When not null tell the server to rename this character. Item1 is the character to rename, Item2 is the new name, Item3 indicates whether the renamed character is already a part of the crew.</param>
/// <param name="firedCharacter">When not null tell the server to fire this character</param>
/// <param name="validateHires">When set to true will tell the server to validate pending hires</param>
public void SendCrewState(bool updatePending, (CharacterInfo info, string newName) renameCharacter = default, CharacterInfo firedCharacter = null, bool validateHires = false)
public void SendCrewState(bool updatePending = false, (CharacterInfo info, string newName) renameCharacter = default, CharacterInfo firedCharacter = null, bool validateHires = false)
{
if (campaign is MultiPlayerCampaign)
{
@@ -1078,6 +1245,7 @@ namespace Barotrauma
foreach (CharacterInfo pendingHire in PendingHires)
{
msg.WriteUInt16(pendingHire.ID);
msg.WriteBoolean(pendingHire.BotStatus == BotStatus.PendingHireToReserveBench);
}
}
@@ -1089,7 +1257,9 @@ namespace Barotrauma
{
msg.WriteUInt16(renameCharacter.info.ID);
msg.WriteString(renameCharacter.newName);
bool existingCrewMember = campaign.CrewManager?.GetCharacterInfos().Any(ci => ci.ID == renameCharacter.info.ID) ?? false;
bool existingCrewMember =
campaign.CrewManager is CrewManager crewManager &&
crewManager.GetCharacterInfos(includeReserveBench: true).Any(ci => ci.ID == renameCharacter.info.ID);
msg.WriteBoolean(existingCrewMember);
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Microsoft.Xna.Framework.Input;
using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement;
namespace Barotrauma
@@ -1562,7 +1563,7 @@ namespace Barotrauma
bool locationHasDealOnItem = isSellingRelatedList ?
ActiveStore.RequestedGoods.Contains(pi.ItemPrefab) : ActiveStore.DailySpecials.Contains(pi.ItemPrefab);
GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), nameAndQuantityGroup.RectTransform),
pi.ItemPrefab.Name, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft)
RichString.Rich(pi.ItemPrefab.Name), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft)
{
CanBeFocused = false,
Shadow = locationHasDealOnItem,
@@ -1573,15 +1574,24 @@ namespace Barotrauma
if (locationHasDealOnItem)
{
var relativeWidth = (0.9f * nameAndQuantityFrame.Rect.Height) / nameAndQuantityFrame.Rect.Width;
Vector2 dealIconSize = new Vector2(relativeWidth, 0.9f) * 0.5f;
var dealIcon = new GUIImage(
new RectTransform(new Vector2(relativeWidth, 0.9f), nameAndQuantityFrame.RectTransform, anchor: Anchor.CenterLeft)
new RectTransform(dealIconSize, nameAndQuantityFrame.RectTransform, anchor: Anchor.CenterRight)
{
AbsoluteOffset = new Point((int)nameBlock.Padding.X, 0)
},
"StoreDealIcon", scaleToFit: true)
{
CanBeFocused = false
CanBeFocused = false,
UserData = "StoreDealIcon"
};
var dealIconColor = dealIcon.Color;
if (forceDisable)
{
dealIconColor.A = 0;
}
dealIcon.Color = dealIconColor;
dealIcon.SetAsFirstChild();
}
bool isParentOnLeftSideOfInterface = parentComponent == storeBuyList || parentComponent == storeDailySpecialsGroup ||
@@ -1713,7 +1723,7 @@ namespace Barotrauma
mainGroup.Recalculate();
mainGroup.RectTransform.RecalculateChildren(true, true);
amountInput?.LayoutGroup.Recalculate();
nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width);
nameBlock.Text = ToolBox.LimitString(nameBlock.Text.SanitizedString, nameBlock.Font, nameBlock.Rect.Width);
mainGroup.RectTransform.Children.ForEach(c => c.IsFixedSize = true);
return frame;
@@ -1795,6 +1805,9 @@ namespace Barotrauma
private void SetItemFrameStatus(GUIComponent itemFrame, bool enabled)
{
float full = 1f;
float dim = 0.7f;
float alpha = (enabled ? full : dim);
if (itemFrame?.UserData is not PurchasedItem pi) { return; }
bool refreshFrameStatus = !pi.IsStoreComponentEnabled.HasValue || pi.IsStoreComponentEnabled.Value != enabled;
if (!refreshFrameStatus) { return; }
@@ -1802,14 +1815,14 @@ namespace Barotrauma
{
if (pi.ItemPrefab?.InventoryIcon != null)
{
icon.Color = pi.ItemPrefab.InventoryIconColor * (enabled ? 1.0f : 0.5f);
icon.Color = pi.ItemPrefab.InventoryIconColor * alpha;
}
else if (pi.ItemPrefab?.Sprite != null)
{
icon.Color = pi.ItemPrefab.SpriteColor * (enabled ? 1.0f : 0.5f);
icon.Color = pi.ItemPrefab.SpriteColor * alpha;
}
};
var color = Color.White * (enabled ? 1.0f : 0.5f);
var color = Color.White * alpha;
if (itemFrame.FindChild("name", recursive: true) is GUITextBlock name)
{
name.TextColor = color;
@@ -1835,7 +1848,7 @@ namespace Barotrauma
}
if (itemFrame.FindChild("price", recursive: true) is GUITextBlock priceBlock)
{
priceBlock.TextColor = isDiscounted ? storeSpecialColor * (enabled ? 1.0f : 0.5f) : color;
priceBlock.TextColor = isDiscounted ? storeSpecialColor * alpha : color;
}
if (itemFrame.FindChild("addbutton", recursive: true) is GUIButton addButton)
{
@@ -1845,6 +1858,10 @@ namespace Barotrauma
{
removeButton.Enabled = enabled;
}
if (itemFrame.FindChild("StoreDealIcon", recursive: true) is GUIImage dealIcon)
{
dealIcon.Color = dealIcon.Color * alpha;
}
pi.IsStoreComponentEnabled = enabled;
itemFrame.UserData = pi;
}
@@ -2271,6 +2288,15 @@ namespace Barotrauma
{
updateStopwatch.Restart();
if (GameMain.DevMode)
{
if (PlayerInput.KeyDown(Keys.D0))
{
CreateUI();
needsRefresh = true;
}
}
if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y)
{
CreateUI();

View File

@@ -742,8 +742,8 @@ namespace Barotrauma
private (LocalizedString header, LocalizedString body) GetItemTransferWarningText()
{
var header = TextManager.Get("itemtransferheader").Fallback("lowfuelheader", useDefaultLanguageIfFound: false);
var body = TextManager.Get("itemtransferwarning").Fallback("lowfuelwarning", useDefaultLanguageIfFound: false);
var header = TextManager.Get("itemtransferheader").Fallback(TextManager.Get("lowfuelheader"), useDefaultLanguageIfFound: false);
var body = TextManager.Get("itemtransferwarning").Fallback(TextManager.Get("lowfuelwarning"), useDefaultLanguageIfFound: false);
return (header, body);
}

View File

@@ -120,9 +120,20 @@ namespace Barotrauma
{
if (Client == null) { return; }
if (currentPing == Client.Ping) { return; }
currentPing = Client.Ping;
textBlock.Text = currentPing.ToString();
textBlock.TextColor = GetPingColor();
if (GameMain.NetworkMember != null && GameMain.NetworkMember.ConnectedClients.Contains(Client))
{
currentPing = Client.Ping;
textBlock.Text = currentPing.ToString();
textBlock.TextColor = GetPingColor();
textBlock.ToolTip = string.Empty;
}
else
{
currentPing = 0;
textBlock.Text = "-";
textBlock.TextColor = GUIStyle.Red;
textBlock.ToolTip = TextManager.Get("causeofdeathdescription.disconnected");
}
}
public void TryPermissionIconRefresh(Sprite icon)
@@ -416,7 +427,10 @@ namespace Barotrauma
=> TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)MathF.Round(value)}");
}
var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine");
if (Submarine.MainSub != null)
{
createTabButton(InfoFrameTab.Submarine, "submarine");
}
var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.character");
talentsButton.OnAddedToGUIUpdateList += (component) =>
@@ -458,17 +472,19 @@ namespace Barotrauma
CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub);
break;
case InfoFrameTab.Talents:
talentMenu.CreateGUI(infoFrameHolder, Character.Controlled ?? GameMain.Client?.Character);
talentMenu.CreateGUI(infoFrameHolder, Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo);
break;
}
}
private const float jobColumnWidthPercentage = 0.138f,
characterColumnWidthPercentage = 0.45f,
pingColumnWidthPercentage = 0.206f,
walletColumnWidthPercentage = 0.206f;
private const float JobColumnWidthPercentage = 0.138f,
CharacterColumnWidthPercentage = 0.45f,
KillColumnWidthPercentage = 0.1f,
DeathColumnWidthPercentage = 0.1f,
PingColumnWidthPercentage = 0.15f,
WalletColumnWidthPercentage = 0.206f;
private int jobColumnWidth, characterColumnWidth, pingColumnWidth, walletColumnWidth;
private int jobColumnWidth, characterColumnWidth, pingColumnWidth, walletColumnWidth, deathColumnWidth, killColumnWidth;
private void CreateCrewListFrame(GUIFrame crewFrame)
{
@@ -496,11 +512,21 @@ namespace Barotrauma
{
if (teamIDs.Count > 1)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: i == 0 ? GUIStyle.Green : GUIStyle.Orange) { ForceUpperCase = ForceUpperCase.Yes };
var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: CombatMission.GetTeamColor(teamIDs[i]))
{
ForceUpperCase = ForceUpperCase.Yes
};
var teamIcon = new GUIImage(new RectTransform(Vector2.One, nameText.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight),
style: teamIDs[i] == CharacterTeamType.Team2 ? "SeparatistIcon" : "CoalitionIcon")
{
Color = nameText.TextColor
};
nameText.Padding = new Vector4(teamIcon.Rect.Width + nameText.Padding.X, nameText.Padding.Y, nameText.Padding.Z, nameText.Padding.W);
}
headerFrames[i] = new GUILayoutGroup(new RectTransform(Vector2.Zero, content.RectTransform, Anchor.TopLeft, Pivot.BottomLeft) { AbsoluteOffset = new Point(2, -1) }, isHorizontal: true)
{
Stretch = true,
AbsoluteSpacing = 2,
UserData = i
};
@@ -587,8 +613,8 @@ namespace Barotrauma
sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width;
jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f);
characterButton.RectTransform.RelativeSize = new Vector2((1f - jobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f);
jobButton.RectTransform.RelativeSize = new Vector2(JobColumnWidthPercentage * sizeMultiplier, 1f);
characterButton.RectTransform.RelativeSize = new Vector2((1f - JobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f);
jobButton.TextBlock.Font = characterButton.TextBlock.Font = GUIStyle.HotkeyFont;
jobButton.CanBeFocused = characterButton.CanBeFocused = false;
@@ -626,7 +652,8 @@ namespace Barotrauma
var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
{
AbsoluteSpacing = 2
AbsoluteSpacing = 2,
Stretch = true
};
new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect))
@@ -639,21 +666,29 @@ namespace Barotrauma
GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor);
paddedFrame.Recalculate();
linkedGUIList.Add(new LinkedGUI(character, frame, textBlock: null));
}
private void CreateMultiPlayerListContentHolder(GUILayoutGroup headerFrame)
{
bool isCampaign = GameMain.GameSession?.Campaign is MultiPlayerCampaign;
GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale");
GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale");
GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale");
GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(JobColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale");
GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(CharacterColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale");
if (GameMain.GameSession?.GameMode is PvPMode)
{
var killButton = new GUIButton(new RectTransform(new Vector2(KillColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("killcount"), style: "GUIButtonSmallFreeScale");
killColumnWidth = killButton.Rect.Width;
var deathButton = new GUIButton(new RectTransform(new Vector2(DeathColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("deathcount"), style: "GUIButtonSmallFreeScale");
deathColumnWidth = deathButton.Rect.Width;
}
GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(PingColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale");
if (isCampaign)
{
GUIButton walletButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform)
{
RelativeSize = new Vector2(walletColumnWidthPercentage * sizeMultiplier, 1f)
}, TextManager.Get("crewwallet.wallet"), style: "GUIButtonSmallFreeScale")
GUIButton walletButton = new GUIButton(new RectTransform(new Vector2(WalletColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("crewwallet.wallet"), style: "GUIButtonSmallFreeScale")
{
TextBlock = { Font = GUIStyle.HotkeyFont },
CanBeFocused = false,
@@ -662,15 +697,12 @@ namespace Barotrauma
walletColumnWidth = walletButton.Rect.Width;
}
sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width;
jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f);
characterButton.RectTransform.RelativeSize = new Vector2((characterColumnWidthPercentage + (isCampaign ? 0 : walletColumnWidthPercentage)) * sizeMultiplier, 1f);
pingButton.RectTransform.RelativeSize = new Vector2(pingColumnWidthPercentage * sizeMultiplier, 1f);
jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUIStyle.HotkeyFont;
jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = false;
jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = ForceUpperCase.Yes;
foreach (var btn in headerFrame.GetAllChildren<GUIButton>())
{
btn.TextBlock.Font = GUIStyle.HotkeyFont;
btn.CanBeFocused = false;
btn.ForceUpperCase = ForceUpperCase.Yes;
}
jobColumnWidth = jobButton.Rect.Width;
characterColumnWidth = characterButton.Rect.Width;
@@ -688,45 +720,68 @@ namespace Barotrauma
var connectedClients = GameMain.Client.ConnectedClients;
for (int i = 0; i < teamIDs.Count; i++)
for (int teamID = 0; teamID < teamIDs.Count; teamID++)
{
foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i]))
foreach (Character character in crew.Where(c => c.TeamID == teamIDs[teamID]))
{
if (!(character is AICharacter) && connectedClients.Any(c => c.Character == null && c.Name == character.Name)) { continue; }
CreateMultiPlayerCharacterElement(character, GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.Character == character), i);
if (character is not AICharacter && connectedClients.Any(c => c.Character == null && c.Name == character.Name)) { continue; }
CreateMultiPlayerCharacterElement(character, GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.Character == character), teamID);
}
foreach (CharacterInfo characterInfo in GameMain.GameSession.CrewManager?.GetReserveBenchInfos() ?? Enumerable.Empty<CharacterInfo>())
{
CreateMultiPlayerCharacterElement(character: null, client: null, teamID, justCharacterInfo: characterInfo);
}
}
for (int j = 0; j < connectedClients.Count; j++)
{
Client client = connectedClients[j];
if (!client.InGame || client.Character == null || client.Character.IsDead)
if (client.Character == null || client.Character.IsDead)
{
CreateMultiPlayerClientElement(client);
}
}
}
private void CreateMultiPlayerCharacterElement(Character character, Client client, int i)
/// <param name="justCharacterInfo">The character element can be generated based on just a CharacterInfo, and Character and Client can be left null. Otherwise, those are required and the CharacterInfo of the Character is used.</param>
private void CreateMultiPlayerCharacterElement(Character character, Client client, int teamID, CharacterInfo justCharacterInfo = null)
{
GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[i].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[i].Content.RectTransform), style: "ListBoxElement")
CharacterInfo characterInfo = justCharacterInfo ?? character.Info;
GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[teamID].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[teamID].Content.RectTransform), style: "ListBoxElement")
{
UserData = character,
UserData = character != null ? character : characterInfo,
Color = (GameMain.NetworkMember != null && GameMain.Client.Character == character) ? OwnCharacterBGColor : Color.Transparent
};
frame.OnSecondaryClicked += (component, data) =>
if (client != null)
{
NetLobbyScreen.CreateModerationContextMenu(client);
return true;
};
frame.OnSecondaryClicked += (component, data) =>
{
NetLobbyScreen.CreateModerationContextMenu(client);
return true;
};
}
var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
{
AbsoluteSpacing = 2
AbsoluteSpacing = 2,
Stretch = true
};
new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect))
new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center),
onDraw: (sb, component) =>
{
if (client == null)
{
characterInfo?.DrawJobIcon(sb, component.Rect);
}
else
{
DrawClientJobIcon(sb, component.Rect, client);
}
})
{
CanBeFocused = false,
HoverColor = Color.White,
@@ -736,6 +791,19 @@ namespace Barotrauma
if (client != null)
{
CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon);
if (GameMain.GameSession?.GameMode is PvPMode)
{
new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientKillCount(client) ?? 0).ToString()
};
new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientDeathCount(client) ?? 0).ToString()
};
}
linkedGUIList.Add(new LinkedGUI(client, frame,
new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center),
permissionIcon));
@@ -743,27 +811,62 @@ namespace Barotrauma
else
{
GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor);
ToolBox.LimitString(characterInfo.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: characterInfo.Job.Prefab.UIColor);
if (GameMain.GameSession?.GameMode is PvPMode)
{
new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetBotKillCount(characterInfo) ?? 0).ToString()
};
new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetBotDeathCount(characterInfo) ?? 0).ToString()
};
}
if (character is AICharacter)
{
// "BOT" instead of ping (which isn't relevant for bots)
linkedGUIList.Add(new LinkedGUI(character, frame,
new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = ForceUpperCase.Yes }));
new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = ForceUpperCase.Yes }));
}
else
else if (characterInfo.IsOnReserveBench)
{
linkedGUIList.Add(new LinkedGUI(client: null, frame, textBlock: null, permissionIcon: null));
new GUICustomComponent(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => DrawDisconnectedIcon(sb, component.Rect))
// Reserve bench icon
new GUIImage(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height - 4), paddedFrame.RectTransform), style: "CrewManagementReserveBenchIconReserve", scaleToFit: true)
{
CanBeFocused = false,
HoverColor = Color.White,
SelectedColor = Color.White
ToolTip = TextManager.Get("ReserveBenchStatus.Reserve")
};
}
if (characterInfo.IsOnReserveBench)
{
//black bar to dim out the elements (1px shorter and to the right so it won't dim the left border too)
new GUIFrame(
new RectTransform(new Point(paddedFrame.Rect.Width - 1, frame.Rect.Height), paddedFrame.RectTransform, Anchor.Center)
{
AbsoluteOffset = new Point(1, 0)
},
style: null, color: Color.Black * 0.7f)
{
IgnoreLayoutGroups = true,
CanBeFocused = false
};
}
}
CreateWalletCrewFrame(character, paddedFrame);
if (character != null)
{
CreateWalletCrewFrame(character, paddedFrame);
}
else if (characterInfo.IsOnReserveBench)
{
// Empty column for reserve benched bots
new GUILayoutGroup(new RectTransform(new Point(walletColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.Center) { CanBeFocused = false };
}
paddedFrame.Recalculate();
}
private void CreateMultiPlayerClientElement(Client client)
@@ -785,11 +888,12 @@ namespace Barotrauma
var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
{
AbsoluteSpacing = 2
AbsoluteSpacing = 2,
Stretch = true
};
new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center),
onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client))
onDraw: (sb, component) => DrawClientJobIcon(sb, component.Rect, client))
{
CanBeFocused = false,
HoverColor = Color.White,
@@ -797,14 +901,26 @@ namespace Barotrauma
};
CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon);
if (GameMain.GameSession?.GameMode is PvPMode)
{
new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientKillCount(client) ?? 0).ToString()
};
new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientDeathCount(client) ?? 0).ToString()
};
}
linkedGUIList.Add(new LinkedGUI(client, frame,
new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center),
permissionIcon));
if (client.Character is { } character)
{
CreateWalletCrewFrame(character, paddedFrame);
}
CreateWalletCrewFrame(client.Character, paddedFrame);
paddedFrame.Recalculate();
}
private int GetTeamIndex(Client client)
@@ -837,12 +953,12 @@ namespace Barotrauma
}
}
return 0;
return teamIDs.IndexOf(client.TeamID);
}
private void CreateWalletCrewFrame(Character character, GUILayoutGroup paddedFrame)
{
if (!(GameMain.GameSession?.Campaign is MultiPlayerCampaign)) { return; }
if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign) { return; }
GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(new Point(walletColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.Center)
{
@@ -860,7 +976,7 @@ namespace Barotrauma
ToolTip = TextManager.Get("walletdescription")
};
if (character.IsBot) { return; }
if (character == null || character.IsBot) { return; }
Sprite walletSprite = GUIStyle.CrewWalletIconSmall.Value.Sprite;
@@ -948,7 +1064,6 @@ namespace Barotrauma
int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUIStyle.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width);
permissionIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 1f), paddedFrame.RectTransform) { AbsoluteOffset = new Point(xOffset + 2, 0) }, permissionIconSprite) { IgnoreLayoutGroups = true };
if (client.Character != null && client.Character.IsDead)
{
characterNameBlock.Strikethrough = new GUITextBlock.StrikethroughSettings(null, GUI.IntScale(1f), GUI.IntScale(5f));
@@ -969,18 +1084,15 @@ namespace Barotrauma
}
}
private void DrawNotInGameIcon(SpriteBatch spriteBatch, Rectangle area, Client client)
private void DrawClientJobIcon(SpriteBatch spriteBatch, Rectangle area, Client client)
{
if (client.Spectating)
{
spectateIcon.Draw(spriteBatch, area, Color.White);
}
else if (client.Character != null && client.Character.IsDead)
else if (client.Character != null && client.InGame)
{
if (client.Character.Info != null)
{
client.Character.Info.DrawJobIcon(spriteBatch, area);
}
client.Character.Info?.DrawJobIcon(spriteBatch, area);
}
else
{
@@ -1005,6 +1117,12 @@ namespace Barotrauma
GUIComponent existingPreview = infoFrameHolder.FindChild("SelectedCharacter");
if (existingPreview != null) { infoFrameHolder.RemoveChild(existingPreview); }
if (userData is CharacterInfo { IsOnReserveBench: true })
{
return true;
}
// Modal info panel that pops up on the right
GUIFrame background = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.69f), infoFrameHolder.RectTransform, Anchor.TopRight, Pivot.TopLeft) { RelativeOffset = new Vector2(-0.061f, 0) })
{
UserData = "SelectedCharacter"
@@ -1028,7 +1146,7 @@ namespace Barotrauma
{
talentButton.OnClicked = (button, o) =>
{
talentMenu.CreateGUI(infoFrameHolder, character);
talentMenu.CreateGUI(infoFrameHolder, character.Info);
return true;
};
}
@@ -1413,7 +1531,7 @@ namespace Barotrauma
var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.322f), paddedFrame.RectTransform), isHorizontal: true);
new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform),
onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client));
onDraw: (sb, component) => DrawClientJobIcon(sb, component.Rect, client));
GUIFont font = paddedFrame.Rect.Width < 280 ? GUIStyle.SmallFont : GUIStyle.Font;
@@ -1503,6 +1621,11 @@ namespace Barotrauma
}
linkedGUIList.Clear();
foreach (GUIListBox crewList in crewListArray)
{
crewList.Content.ClearChildren();
}
}
private void AddLineToLog(string line, PlayerConnectionChangeType type)
@@ -1587,7 +1710,7 @@ namespace Barotrauma
}
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.DisplayName, font: GUIStyle.LargeFont);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.GetLocationTypeToDisplay().Name, font: GUIStyle.SubHeadingFont);
if (location.Faction?.Prefab != null)
{
@@ -1633,6 +1756,7 @@ namespace Barotrauma
textContent,
mission.Difficulty ?? 0,
mission.Prefab.Icon, mission.Prefab.IconColor,
mission.GetDifficultyToolTipText(),
out GUIImage missionIcon);
if (missionIcon != null)
{
@@ -1663,6 +1787,8 @@ namespace Barotrauma
private static void CreateSubmarineInfo(GUIFrame infoFrame, Submarine sub)
{
if (sub == null) { return; }
GUIFrame subInfoFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
GUIFrame paddedFrame = new GUIFrame(new RectTransform(Vector2.One * 0.97f, subInfoFrame.RectTransform, Anchor.Center), style: null);
@@ -1765,7 +1891,7 @@ namespace Barotrauma
{
parent.Content.ClearChildren();
List<GUITextBlock> skillNames = new List<GUITextBlock>();
foreach (Skill skill in info.Job.GetSkills())
foreach (Skill skill in info.Job.GetSkills().OrderByDescending(static s => s.Level))
{
GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.0f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = true };
var skillName = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}").Fallback(skill.Identifier.Value));

View File

@@ -7,6 +7,7 @@ using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using static Barotrauma.TalentTree;
using static Barotrauma.TalentTree.TalentStages;
@@ -83,17 +84,21 @@ namespace Barotrauma
private GUIButton? talentApplyButton,
talentResetButton;
public void CreateGUI(GUIFrame parent, Character? targetCharacter)
private delegate void StartAnimation(RectangleF start, RectangleF end, float duration);
private StartAnimation? startAnimation;
private GUIComponent? talentMainArea;
public void CreateGUI(GUIFrame parent, CharacterInfo? characterInfo)
{
this.characterInfo = characterInfo;
character = characterInfo?.Character;
parent.ClearChildren();
talentButtons.Clear();
talentShowCaseButtons.Clear();
talentCornerIcons.Clear();
showCaseTalentFrames.Clear();
character = targetCharacter;
characterInfo = targetCharacter?.Info;
GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
int padding = GUI.IntScale(15);
GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), parent.RectTransform, Anchor.Center), style: null);
@@ -136,7 +141,7 @@ namespace Barotrauma
GUILayoutGroup playerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), containerFrame.RectTransform, Anchor.TopCenter));
GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false);
if (!GameMain.NetLobbyScreen.PermadeathMode)
if (!GameMain.NetLobbyScreen.PermadeathMode && GameMain.GameSession?.GameMode is not PvPMode)
{
GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall")
@@ -294,7 +299,7 @@ namespace Barotrauma
}
ImmutableHashSet<TalentPrefab?> talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(static e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)).ToImmutableHashSet();
if (talentsOutsideTree.Any())
if (talentsOutsideTree.Any(static t => t != null && !t.IsHiddenExtraTalent))
{
//spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), nameLayout.RectTransform), style: null);
@@ -319,6 +324,7 @@ namespace Barotrauma
foreach (var extraTalent in talentsOutsideTree)
{
if (extraTalent is null) { continue; }
if (extraTalent.IsHiddenExtraTalent) { continue; }
GUIImage talentImg = new GUIImage(new RectTransform(Vector2.One, extraTalentList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite: extraTalent.Icon, scaleToFit: true)
{
ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(extraTalent.Description.Value)),
@@ -341,7 +347,15 @@ namespace Barotrauma
private void CreateTalentMenu(GUIComponent parent, CharacterInfo info, TalentTree tree)
{
GUIListBox mainList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), parent.RectTransform, anchor: Anchor.TopCenter));
talentMainArea = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), parent.RectTransform, Anchor.TopCenter), style: null );
GUIListBox mainList = new GUIListBox(new RectTransform(Vector2.One, talentMainArea.RectTransform));
startAnimation = CreatePopupAnimationHandler(talentMainArea);
if (info is { TalentRefundPoints: > 0, ShowTalentResetPopupOnOpen: true })
{
CreateTalentResetPopup(talentMainArea);
}
selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet();
@@ -425,6 +439,130 @@ namespace Barotrauma
}
}
private void CreateTalentResetPopup(GUIComponent parent)
{
int talentResetCount = 0;
if (character?.Info != null)
{
talentResetCount = Math.Min(character.Info.TalentResetCount, character.Info.GetCurrentLevel());
}
bool hasResetTalentsBefore = talentResetCount > 0;
var bgBlocker = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, anchor: Anchor.Center), style: "GUIBackgroundBlocker")
{
IgnoreLayoutGroups = true
};
var popup = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.8f), bgBlocker.RectTransform, Anchor.Center));
var popupLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(popup.RectTransform, 0.95f), popup.RectTransform, Anchor.Center), isHorizontal: false);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), popupLayout.RectTransform), TextManager.Get("talentresetheader"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center);
new GUITextBlock(new RectTransform(new Vector2(1.0f, hasResetTalentsBefore ? 0.25f : 0.5f), popupLayout.RectTransform), TextManager.Get("talentresetprompt"), wrap: true);
if (hasResetTalentsBefore)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), popupLayout.RectTransform),
TextManager.GetWithVariable("talentresetpromptwarning", "[count]", talentResetCount.ToString()), wrap: true)
{
TextColor = GUIStyle.Red
};
}
var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), popupLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true);
var confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), TextManager.Get("holdtoconfirm"))
{
RequireHold = true,
HoldDurationSeconds = 1.5f,
OnClicked = (button, o) =>
{
if (character is null || characterInfo is null) { return false; }
characterInfo.RefundTalents();
selectedTalents.Clear();
UpdateTalentInfo();
bgBlocker.Visible = false;
return true;
}
};
var denyButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), TextManager.Get("decidelater"))
{
RequireHold = false,
OnClicked = (button, userData) =>
{
if (talentResetButton is not { } resetButton) { return false; }
startAnimation?.Invoke(popup.Rect, resetButton.Rect, 0.25f);
resetButton.Flash(GUIStyle.Green);
bgBlocker.Visible = false;
if (characterInfo != null)
{
characterInfo.ShowTalentResetPopupOnOpen = false;
}
return true;
}
};
}
private static StartAnimation CreatePopupAnimationHandler(GUIComponent parent)
{
bool drawAnimation = false;
float animDur = 1f,
animTimer = 0f;
RectangleF drawRect = RectangleF.Empty,
animStartRect = RectangleF.Empty,
animEndRect = RectangleF.Empty;
void StartAnimation(RectangleF start, RectangleF end, float duration)
{
animStartRect = start;
animEndRect = end;
animTimer = 0;
animDur = duration;
drawRect = start;
drawAnimation = true;
}
void OnDraw(SpriteBatch batch, GUICustomComponent component)
{
if (!drawAnimation) { return; }
GUIComponentStyle style = GUIStyle.GetComponentStyle("GUIFrame");
style.Sprites[GUIComponent.ComponentState.None][0].Draw(batch, drawRect, Color.White);
}
void OnUpdate(float f, GUICustomComponent component)
{
if (!drawAnimation) { return; }
animTimer += f;
if (animTimer > animDur)
{
drawRect = animEndRect;
drawAnimation = false;
return;
}
float lerp = animTimer / animDur;
drawRect = new RectangleF(
MathHelper.Lerp(animStartRect.X, animEndRect.X, lerp),
MathHelper.Lerp(animStartRect.Y, animEndRect.Y, lerp),
MathHelper.Lerp(animStartRect.Width, animEndRect.Width, lerp),
MathHelper.Lerp(animStartRect.Height, animEndRect.Height, lerp));
}
new GUICustomComponent(new RectTransform(Vector2.One, parent.RectTransform), onDraw: OnDraw, onUpdate: OnUpdate)
{
IgnoreLayoutGroups = true,
CanBeFocused = false
};
return StartAnimation;
}
private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree, int index, TalentOption talentOption, CharacterInfo info, int specializationCount)
{
int elementPadding = GUI.IntScale(8);
@@ -679,6 +817,15 @@ namespace Barotrauma
private bool ResetTalentSelection(GUIButton guiButton, object userData)
{
if (characterInfo is null) { return false; }
int newTalentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
// if we don't have talents selected, and we have points to refund, show the refund popup
if (characterInfo.TalentRefundPoints > 0 && newTalentCount == 0)
{
CreateTalentResetPopup(talentMainArea!);
return true;
}
selectedTalents = characterInfo.GetUnlockedTalentsInTree().ToHashSet();
UpdateTalentInfo();
return true;
@@ -844,12 +991,31 @@ namespace Barotrauma
}
}
private static readonly LocalizedString refundText = TextManager.Get("refund"),
resetText = TextManager.Get("reset");
public void Update()
{
if (characterInfo is null || talentResetButton is null || talentApplyButton is null) { return; }
int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0;
talentApplyButton.Enabled = character != null && talentCount > 0;
talentResetButton.Enabled = character != null && (talentCount > 0 || characterInfo.TalentRefundPoints > 0);
if (talentCount == 0 && characterInfo.TalentRefundPoints > 0)
{
if (talentResetButton.FlashTimer <= 0.0f)
{
talentResetButton.Flash(GUIStyle.Orange);
}
talentResetButton.Text = refundText;
}
else
{
talentResetButton.Text = resetText;
}
if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f)
{
talentApplyButton.Flash(GUIStyle.Orange);
@@ -893,6 +1059,22 @@ namespace Barotrauma
return info.GetIdentifierUsingOriginalName() == ownCharacterInfo.GetIdentifierUsingOriginalName();
}
private static bool IsOnSameTeam(CharacterInfo? info)
{
if (info is null) { return false; }
CharacterTeamType? ownCharacterTeam = Character.Controlled?.TeamID ?? GameMain.Client?.MyClient?.TeamID;
if (ownCharacterTeam is null) { return false; }
return info.TeamID == ownCharacterTeam;
}
private static bool IsSpectatingInMultiplayer()
{
if (GameMain.Client?.MyClient is not { } myClient) { return false; }
return myClient.Spectating;
}
public static bool CanManageTalents(CharacterInfo targetInfo)
{
// in singleplayer we can do whatever we want
@@ -901,10 +1083,16 @@ namespace Barotrauma
// always allow managing talents for own character
if (IsOwnCharacter(targetInfo)) { return true; }
// disallow managing talents while spectating
if (IsSpectatingInMultiplayer()) { return false; }
// don't allow controlling non-bot characters
if (targetInfo.Character is not { IsBot: true }) { return false; }
// lastly check if we have the permission to do this
// only allow managing talents for bots on the same team
if (!IsOnSameTeam(targetInfo)) { return false; }
// lastly, check if we have the permission to do this
return GameMain.Client is { } client && client.HasPermission(ClientPermissions.ManageBotTalents);
}
}

View File

@@ -127,9 +127,10 @@ namespace Barotrauma
Campaign.OnMoneyChanged.RegisterOverwriteExisting(eventId, _ => RequestRefresh());
}
public void RequestRefresh()
public void RequestRefresh(bool refreshUpgrades = false)
{
needsRefresh = true;
if (refreshUpgrades) { SelectTab(UpgradeTab.Upgrade); }
}
private void RefreshAll()
@@ -673,6 +674,10 @@ namespace Barotrauma
}
}
}
if (!upgrades.ContainsKey(category) && HasSwappableItems(category))
{
upgrades.Add(category, new List<UpgradePrefab>());
}
}
foreach (var (category, prefabs) in upgrades)
@@ -771,19 +776,29 @@ namespace Barotrauma
{
if (Submarine.MainSub == null) { return false; }
subItems ??= GetSubItems();
return subItems.Any(i =>
i.Prefab.SwappableItem != null &&
!i.IsHidden && i.AllowSwapping &&
(i.Prefab.SwappableItem.CanBeBought || ItemPrefab.Prefabs.Any(ip => ip.SwappableItem?.ReplacementOnUninstall == i.Prefab.Identifier)) &&
Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && category.ItemTags.Any(t => i.HasTag(t)));
return subItems.Any(item => HasSwappableItems(category, item));
}
private static bool HasSwappableItems(UpgradeCategory category, Item item)
{
if (Submarine.MainSub == null) { return false; }
return
item.Prefab.SwappableItem != null &&
!item.IsHidden && item.AllowSwapping &&
(item.Prefab.SwappableItem.CanBeBought || ItemPrefab.Prefabs.Any(ip => ip.SwappableItem?.ReplacementOnUninstall == item.Prefab.Identifier)) &&
Submarine.MainSub.IsEntityFoundOnThisSub(item, true) && category.ItemTags.Any(t => item.HasTag(t));
}
private static List<Item> GetSubItems() => Submarine.MainSub?.GetItems(true) ?? new List<Item>();
private void SelectUpgradeCategory(List<UpgradePrefab> prefabs, UpgradeCategory category, Submarine submarine)
{
if (selectedUpgradeCategoryLayout == null) { return; }
bool hasSwappableItems = HasSwappableItems(category);
bool hasUpgradeModules = prefabs.Count > 0;
customizeTabOpen = !hasUpgradeModules && hasSwappableItems;
customizeTabOpen = false;
GUIComponent[] categoryFrames = GetFrames(category);
@@ -799,9 +814,7 @@ namespace Barotrauma
GUIFrame frame = new GUIFrame(rectT(1.0f, 0.4f, selectedUpgradeCategoryLayout));
GUIFrame paddedFrame = new GUIFrame(rectT(0.93f, 0.9f, frame, Anchor.Center), style: null);
bool hasSwappableItems = HasSwappableItems(category);
float listHeight = hasSwappableItems ? 0.9f : 1.0f;
float listHeight = hasSwappableItems && hasUpgradeModules ? 0.9f : 1.0f;
GUIListBox prefabList = new GUIListBox(rectT(1.0f, listHeight, paddedFrame, Anchor.BottomLeft))
{
@@ -810,7 +823,8 @@ namespace Barotrauma
ScrollBarVisible = true
};
if (hasSwappableItems)
//both swappable items and upgrade modules -> create 2 tabs
if (hasSwappableItems && hasUpgradeModules)
{
GUILayoutGroup buttonLayout = new GUILayoutGroup(rectT(1.0f, 0.1f, paddedFrame, anchor: Anchor.TopLeft), isHorizontal: true);
@@ -852,8 +866,15 @@ namespace Barotrauma
return true;
};
}
CreateUpgradePrefabList(prefabList, category, prefabs, submarine);
//only either upgrade modules or swappable items -> just create the list
else if (hasUpgradeModules)
{
CreateUpgradePrefabList(prefabList, category, prefabs, submarine);
}
else if (hasSwappableItems)
{
CreateSwappableItemList(prefabList, category, submarine);
}
}
private void CreateUpgradePrefabList(GUIListBox parent, UpgradeCategory category, List<UpgradePrefab> prefabs, Submarine submarine)
@@ -1370,9 +1391,10 @@ namespace Barotrauma
Item[] entitiesOnSub = drawnSubmarine.GetItems(true).Where(i => drawnSubmarine.IsEntityFoundOnThisSub(i, true)).ToArray();
foreach (UpgradeCategory category in UpgradeCategory.Categories)
{
//hide categories with no upgrades in them
if (UpgradePrefab.Prefabs.None(p => p.UpgradeCategories.Contains(category))) { continue; }
if (entitiesOnSub.Any(item => category.CanBeApplied(item, null)))
//hide categories with no upgrades or swappables in them
bool hasSwappableItems = HasSwappableItems(category);
if (!hasSwappableItems && UpgradePrefab.Prefabs.None(p => p.UpgradeCategories.Contains(category))) { continue; }
if (hasSwappableItems || entitiesOnSub.Any(item => category.CanBeApplied(item, null)))
{
yield return category;
}
@@ -1534,7 +1556,7 @@ namespace Barotrauma
description.Padding = new Vector4(description.Padding.X, 24 * GUI.Scale, description.Padding.Z, description.Padding.W);
List<Entity> pointsOfInterest = (from category in UpgradeCategory.Categories from item in submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs)
where category.CanBeApplied(item, null) && item.IsPlayerTeamInteractable select item).Cast<Entity>().Distinct().ToList();
where (category.CanBeApplied(item, null) || HasSwappableItems(category, item)) && item.IsPlayerTeamInteractable select item).Cast<Entity>().Distinct().ToList();
List<ushort> ids = GameMain.GameSession.SubmarineInfo?.LeftBehindDockingPortIDs ?? new List<ushort>();
pointsOfInterest.AddRange(submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs).Where(item => ids.Contains(item.ID)));
@@ -1799,7 +1821,7 @@ namespace Barotrauma
// Disables the parent and only re-enables if the submarine contains valid items
if (!category.IsWallUpgrade && drawnSubmarine?.Info != null)
{
if (UpgradePrefab.Prefabs.None(p => p.UpgradeCategories.Contains(category) && p.GetMaxLevel(drawnSubmarine.Info) > 0))
if (UpgradePrefab.Prefabs.None(p => p.UpgradeCategories.Contains(category) && p.GetMaxLevel(drawnSubmarine.Info) > 0) && !HasSwappableItems(category))
{
parent.ToolTip = TextManager.Get("upgradecategorynotapplicable");
parent.Enabled = false;

View File

@@ -120,6 +120,7 @@ namespace Barotrauma
private readonly GameTime fixedTime;
public Option<ConnectCommand> ConnectCommand = Option<ConnectCommand>.None();
private string clientName;
private static SpriteBatch spriteBatch;
@@ -252,6 +253,16 @@ namespace Barotrauma
try
{
ConnectCommand = Barotrauma.Networking.ConnectCommand.Parse(ConsoleArguments);
string clientNameFlagArg = args.FirstOrDefault(arg => arg.StartsWith("-username"));
if (clientNameFlagArg != null)
{
int nextIndex = args.IndexOf(clientNameFlagArg) + 1;
if (nextIndex < args.Length)
{
clientName = args[nextIndex];
}
}
}
catch (IndexOutOfRangeException e)
{
@@ -313,6 +324,8 @@ namespace Barotrauma
GameSettings.SetCurrentConfig(config);
}
int display = GameSettings.CurrentConfig.Graphics.Display;
GraphicsWidth = GameSettings.CurrentConfig.Graphics.Width;
GraphicsHeight = GameSettings.CurrentConfig.Graphics.Height;
@@ -340,7 +353,7 @@ namespace Barotrauma
GraphicsDeviceManager.PreferredBackBufferFormat = SurfaceFormat.Color;
GraphicsDeviceManager.PreferMultiSampling = false;
GraphicsDeviceManager.SynchronizeWithVerticalRetrace = GameSettings.CurrentConfig.Graphics.VSync;
SetWindowMode(GameSettings.CurrentConfig.Graphics.DisplayMode);
SetWindowMode(GameSettings.CurrentConfig.Graphics.DisplayMode, display);
defaultViewport = new Viewport(0, 0, GraphicsWidth, GraphicsHeight);
@@ -353,8 +366,17 @@ namespace Barotrauma
ResolutionChanged?.Invoke();
}
public void SetWindowMode(WindowMode windowMode)
public void SetWindowMode(WindowMode windowMode, int display)
{
// We can't move the monitor while the window is fullscreen because of a restriction in SDL2, so as a workaround we switch to windowed mode first
var prevDisplayMode = WindowMode;
if (Window.TargetDisplay != display && prevDisplayMode != WindowMode.Windowed)
{
GraphicsDeviceManager.IsFullScreen = false;
GraphicsDeviceManager.ApplyChanges();
}
Window.TargetDisplay = display;
WindowMode = windowMode;
GraphicsDeviceManager.HardwareModeSwitch = windowMode != WindowMode.BorderlessWindowed;
GraphicsDeviceManager.IsFullScreen = windowMode == WindowMode.Fullscreen || windowMode == WindowMode.BorderlessWindowed;
@@ -723,7 +745,7 @@ namespace Barotrauma
fixedTime.IsRunningSlowly = gameTime.IsRunningSlowly;
TimeSpan addTime = new TimeSpan(0, 0, 0, 0, 16);
fixedTime.ElapsedGameTime = addTime;
fixedTime.TotalGameTime.Add(addTime);
fixedTime.TotalGameTime = fixedTime.TotalGameTime.Add(addTime);
base.Update(fixedTime);
PlayerInput.Update(Timing.Step);
@@ -779,7 +801,7 @@ namespace Barotrauma
{
try
{
SaveUtil.LoadGame(saveFiles.OrderBy(file => file.SaveTime).Last().FilePath);
SaveUtil.LoadGame(CampaignDataPath.CreateRegular(saveFiles.OrderBy(file => file.SaveTime).Last().FilePath));
}
catch (Exception e)
{
@@ -804,18 +826,27 @@ namespace Barotrauma
}
MainMenuScreen.Select();
string clientNameString = clientName ?? MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername());
if (connectCommand.SteamLobbyIdOption.TryUnwrap(out var lobbyId))
{
SteamManager.JoinLobby(lobbyId.Value, joinServer: true);
}
else if (connectCommand.NameAndP2PEndpointsOption.TryUnwrap(out var nameAndEndpoint)
&& nameAndEndpoint is { ServerName: var serverName, Endpoints: var endpoints })
else if ((connectCommand.NameAndP2PEndpointsOption.TryUnwrap(out var nameAndEndpoint) && nameAndEndpoint is { ServerName: var serverName, Endpoints: var endpoints }))
{
Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()),
Client = new GameClient(clientNameString,
endpoints.Cast<Endpoint>().ToImmutableArray(),
string.IsNullOrWhiteSpace(serverName) ? endpoints.First().StringRepresentation : serverName,
Option<int>.None());
}
else if ((connectCommand.NameAndLidgrenEndpointOption.TryUnwrap(out var nameAndLidgrenEndpoint) && nameAndLidgrenEndpoint is { ServerName: var lidgrenServerName, Endpoint: var endpoint }))
{
Client = new GameClient(
clientNameString,
endpoint,
string.IsNullOrWhiteSpace(lidgrenServerName) ? endpoint.StringRepresentation : lidgrenServerName,
Option<int>.None());
}
ConnectCommand = Option<ConnectCommand>.None();
}
@@ -1145,7 +1176,7 @@ namespace Barotrauma
}
GameSession.Campaign?.End();
SaveUtil.SaveGame(GameSession.SavePath);
SaveUtil.SaveGame(GameSession.DataPath);
}
if (Client != null)
@@ -1162,11 +1193,12 @@ namespace Barotrauma
GameSession.GameMode?.Preset.Identifier.Value ?? "none",
GameSession.RoundDuration);
string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier.Value ?? "none") + ":";
GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity);
//disabled to reduce the amount of data we collect through GA
/*GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity);
foreach (var activeEvent in GameSession.EventManager.ActiveEvents)
{
GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:ActiveEvents:" + activeEvent.Prefab.Identifier);
}
}*/
GameSession.LogEndRoundStats(eventId);
if (GameSession.GameMode is TutorialMode tutorialMode)
{

View File

@@ -166,7 +166,7 @@ namespace Barotrauma
// check if the store can afford the item
if (store.Balance < itemValue) { continue; }
// TODO: Write logic for prioritizing certain items over others (e.g. lone Battery Cell should be preferred over one inside a Stun Baton)
var matchingItems = sellableItems.Where(i => i.Prefab == item.ItemPrefab);
var matchingItems = sellableItems.Where(i => i.Prefab.Identifier == item.ItemPrefabIdentifier);
int count = Math.Min(item.Quantity, matchingItems.Count());
SoldItem.SellOrigin origin = sellingMode == Store.StoreTab.Sell ? SoldItem.SellOrigin.Character : SoldItem.SellOrigin.Submarine;
if (origin == SoldItem.SellOrigin.Character || GameMain.IsSingleplayer)

View File

@@ -28,7 +28,7 @@ namespace Barotrauma
public GUIComponent ReportButtonFrame { get; set; }
private GUIFrame guiFrame;
private GUIFrame crewArea;
private GUILayoutGroup crewArea;
private GUIListBox crewList;
private float crewListOpenState;
private bool _isCrewMenuOpen = true;
@@ -47,7 +47,7 @@ namespace Barotrauma
/// <summary>
/// This property stores the preference in settings. Don't use for automatic logic.
/// Use AutoShowCrewList(), AutoHideCrewList(), and ResetCrewList().
/// Use AutoHideCrewList(), and ResetCrewList().
/// </summary>
public bool IsCrewMenuOpen
{
@@ -62,11 +62,9 @@ namespace Barotrauma
public static bool PreferCrewMenuOpen = true;
public bool AutoShowCrewList() => _isCrewMenuOpen = true;
public void AutoHideCrewList() => _isCrewMenuOpen = false;
public void ResetCrewList() => _isCrewMenuOpen = PreferCrewMenuOpen;
public void ResetCrewListOpenState() => _isCrewMenuOpen = PreferCrewMenuOpen;
const float CommandNodeAnimDuration = 0.2f;
@@ -93,12 +91,30 @@ namespace Barotrauma
#region Crew Area
crewArea = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.CrewArea, guiFrame.RectTransform), style: null, color: Color.Transparent)
crewArea = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.CrewArea, guiFrame.RectTransform), childAnchor: Anchor.TopCenter)
{
CanBeFocused = false
Stretch = true
};
crewArea.RectTransform.NonScaledSize = HUDLayoutSettings.CrewArea.Size;
for (int i = 0; i < 2; i++)
{
CharacterTeamType teamId = i == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2;
var nameText = new GUITextBlock(new RectTransform(new Point(crewArea.Rect.Width - GUI.IntScale(10), GUI.IntScale(30)), crewArea.RectTransform), CombatMission.GetTeamName(teamId), textColor: CombatMission.GetTeamColor(teamId))
{
ForceUpperCase = ForceUpperCase.Yes,
TextGetter = () => CombatMission.GetTeamName(teamId),
Visible = false,
IgnoreLayoutGroups = true,
UserData = teamId
};
var teamIcon = new GUIImage(new RectTransform(Vector2.One, nameText.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight), style: i == 0 ? "CoalitionIcon" : "SeparatistIcon")
{
Color = nameText.TextColor
};
nameText.Padding = new Vector4(teamIcon.Rect.Width + nameText.Padding.X, nameText.Padding.Y, nameText.Padding.Z, nameText.Padding.W);
}
// AbsoluteOffset is set in UpdateProjectSpecific based on crewListOpenState
crewList = new GUIListBox(new RectTransform(Vector2.One, crewArea.RectTransform), style: null, isScrollBarOnDefaultSide: false)
{
@@ -177,6 +193,12 @@ namespace Barotrauma
ChatBox.InputBox.OnTextChanged += ChatBox.TypingChatMessage;
}
else if (GameMain.Client == null)
{
//this method would throw a non-descriptive nullref exception later when trying to access the chatbox
//if we'd try to continue from here, better to throw a more descriptive one at this point
throw new InvalidOperationException($"Attempted to initialize {nameof(CrewManager)} for multiplayer, but no multiplayer client is active. Are you trying to load a multiplayer save in singleplayer?");
}
#endregion
@@ -290,6 +312,14 @@ namespace Barotrauma
#region Character list management
/// <summary>
/// Note: this is only works client-side. TODO: make it work server-side too?
/// </summary>
public IEnumerable<Character> GetCharacters()
{
return characters;
}
public Rectangle GetActiveCrewArea()
{
return crewArea.Rect;
@@ -562,9 +592,19 @@ namespace Barotrauma
public bool CharacterClicked(GUIComponent component, object selection)
{
if (!AllowCharacterSwitch) { return false; }
if (!(selection is Character character) || character.IsDead || character.IsUnconscious) { return false; }
if (selection is not Character character || character.IsDead || character.IsUnconscious) { return false; }
if (!character.IsOnPlayerTeam) { return false; }
if (GameMain.IsMultiplayer)
{
if (Character.Controlled == null)
{
Camera cam = Screen.Selected.Cam;
cam.Position = character.DrawPosition;
}
return true;
}
SelectCharacter(character);
if (GUI.KeyboardDispatcher.Subscriber == crewList) { GUI.KeyboardDispatcher.Subscriber = null; }
return true;
@@ -631,10 +671,9 @@ namespace Barotrauma
{
if (crewList != this.crewList) { return; }
if (draggedElementData is not Character) { return; }
if (!IsSinglePlayer) { return; }
if (crewList.HasDraggedElementIndexChanged)
{
UpdateCrewListIndices();
if (IsSinglePlayer) { UpdateCrewListIndices(); }
}
else
{
@@ -645,10 +684,13 @@ namespace Barotrauma
private void ResetCrewListIndex(Character c)
{
if (c?.Info == null) { return; }
c.Info.CrewListIndex = -1;
UpdateCrewListIndices();
//default to the bottom of the list
c.Info.CrewListIndex = int.MaxValue;
}
/// <summary>
/// Refresh the <see cref="CharacterInfo.CrewListIndex"/> of the characters based on their order in the crew list
/// </summary>
private void UpdateCrewListIndices()
{
if (crewList == null) { return; }
@@ -661,20 +703,23 @@ namespace Barotrauma
}
}
/// <summary>
/// Order the crew list according to the characters' <see cref="CharacterInfo.CrewListIndex"/>
/// </summary>
private void SortCrewList()
{
if (crewList == null) { return; }
crewList.Content.RectTransform.SortChildren((x, y) =>
{
var infoX = (x.GUIComponent.UserData as Character)?.Info?.CrewListIndex;
var infoY = (y.GUIComponent.UserData as Character)?.Info?.CrewListIndex;
if (infoX.HasValue)
int? index1 = (x.GUIComponent.UserData as Character)?.Info?.CrewListIndex;
int? index2 = (y.GUIComponent.UserData as Character)?.Info?.CrewListIndex;
if (index1.HasValue)
{
return infoY.HasValue ? infoX.Value.CompareTo(infoY.Value) : -1;
return index2.HasValue ? index1.Value.CompareTo(index2.Value) : -1;
}
else
{
return infoY.HasValue ? 1 : 0;
return index2.HasValue ? 1 : 0;
}
});
UpdateCrewListIndices();
@@ -1312,6 +1357,7 @@ namespace Barotrauma
{
if (ConversationAction.IsDialogOpen) { return; }
if (!AllowCharacterSwitch) { return; }
if (character == null || character.Removed) { return; }
//make the previously selected character wait in place for some time
//(so they don't immediately start idling and walking away from their station)
var aiController = Character.Controlled?.AIController;
@@ -1323,6 +1369,16 @@ namespace Barotrauma
{
GameSession.TabMenuInstance.SelectInfoFrameTab(TabMenu.SelectedTab);
}
if (character.SelectedItem?.GetComponent<Controller>() == null && character.SelectedCharacter == null)
{
ResetCrewListOpenState();
ChatBox.ResetChatBoxOpenState();
}
else
{
AutoHideCrewList();
ChatBox.AutoHideChatBox();
}
}
private int TryAdjustIndex(int amount)
@@ -1340,7 +1396,7 @@ namespace Barotrauma
if (index > lastIndex) { index = 0; }
if (index < 0) { index = lastIndex; }
if ((crewList.Content.GetChild(index)?.UserData as Character)?.IsOnPlayerTeam ?? false)
if (crewList.Content.GetChild(index)?.UserData is Character { IsOnPlayerTeam: true, Removed: false })
{
return index;
}
@@ -1635,6 +1691,18 @@ namespace Barotrauma
{
crewArea.Visible = characters.Count > 0 && CharacterHealth.OpenHealthWindow == null;
CharacterTeamType myTeam = Character.Controlled?.TeamID ?? GameMain.Client?.MyClient?.TeamID ?? CharacterTeamType.Team1;
if (GameMain.GameSession?.GameMode is PvPMode)
{
var team1Text = crewArea.GetChildByUserData(CharacterTeamType.Team1);
team1Text.Visible = myTeam == CharacterTeamType.Team1;
team1Text.IgnoreLayoutGroups = !team1Text.Visible;
var team2Text = crewArea.GetChildByUserData(CharacterTeamType.Team2);
team2Text.Visible = myTeam == CharacterTeamType.Team2;
team2Text.IgnoreLayoutGroups = !team2Text.Visible;
}
foreach (GUIComponent characterComponent in crewList.Content.Children)
{
if (characterComponent.UserData is Character character)
@@ -1645,7 +1713,7 @@ namespace Barotrauma
continue;
}
characterComponent.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID;
characterComponent.Visible = myTeam == character.TeamID;
if (character.TeamID == CharacterTeamType.FriendlyNPC && Character.Controlled != null &&
(character.CurrentHull == Character.Controlled.CurrentHull || Vector2.DistanceSquared(Character.Controlled.WorldPosition, character.WorldPosition) < 500.0f * 500.0f))
{
@@ -1873,7 +1941,7 @@ namespace Barotrauma
{
get
{
if (GameMain.GameSession?.CrewManager == null)
if (GameMain.GameSession?.CrewManager == null || Screen.Selected is { IsEditor: true })
{
return false;
}
@@ -2010,7 +2078,7 @@ namespace Barotrauma
// Character context works differently to others as we still use the "basic" command interface,
// but the order will be automatically assigned to this character
isContextual = forceContextual;
if (entityContext is Character character)
if (entityContext is Character { Info: not null } character)
{
characterContext = character;
itemContext = null;
@@ -2098,7 +2166,7 @@ namespace Barotrauma
CreateNodeConnectors();
if (Character.Controlled != null)
{
Character.Controlled.dontFollowCursor = true;
Character.Controlled.FollowCursor = false;
}
HintManager.OnShowCommandInterface();
@@ -2242,7 +2310,7 @@ namespace Barotrauma
returnNodeHotkey = expandNodeHotkey = Keys.None;
if (Character.Controlled != null)
{
Character.Controlled.dontFollowCursor = false;
Character.Controlled.FollowCursor = true;
}
}
@@ -2511,7 +2579,7 @@ namespace Barotrauma
// --> Create shortcut node for Steer order
if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["steer"]) &&
subItems.Find(i => i.HasTag(Tags.NavTerminal) && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedItem == nav) &&
nav.GetComponent<Steering>() is Steering steering && steering.Voltage > steering.MinVoltage)
nav.GetComponent<Steering>() is Steering { HasPower: true } steering)
{
var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering);
AddOrderNode(order);
@@ -2625,9 +2693,9 @@ namespace Barotrauma
bool IsNonDuplicateOrder(Order order) => IsNonDuplicateOrderPrefab(order.Prefab, order.Option);
bool IsNonDuplicateOrderPrefab(OrderPrefab orderPrefab, Identifier option = default)
{
return characterContext == null || (option.IsEmpty ?
return characterContext == null || (characterContext.CurrentOrders != null && (option.IsEmpty ?
characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier) :
characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier && oi.Option == option));
characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier && oi?.Option == option)));
}
void AddOrderNodeWithIdentifier(string identifier)
{
@@ -3000,7 +3068,7 @@ namespace Barotrauma
{
optionElement.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button);
}
var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o != null &&
var colorMultiplier = characters.Any(c => c.CurrentOrders != null && c.CurrentOrders.Any(o => o != null &&
o.Identifier == userData.Order.Identifier &&
o.TargetEntity == userData.Order.TargetEntity)) ? 0.5f : 1f;
CreateNodeIcon(Vector2.One, optionElement.RectTransform, item.Prefab.MinimapIcon ?? order.SymbolSprite, order.Color * colorMultiplier, tooltip: item.Name);
@@ -3654,7 +3722,7 @@ namespace Barotrauma
bool hasLeaks = Character.Controlled.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f);
ToggleReportButton("reportbreach", hasLeaks);
bool hasIntruders = Character.CharacterList.Any(c => c.CurrentHull == Character.Controlled.CurrentHull && AIObjectiveFightIntruders.IsValidTarget(c, Character.Controlled, false));
bool hasIntruders = Character.CharacterList.Any(c => c.CurrentHull == Character.Controlled.CurrentHull && AIObjectiveFightIntruders.IsValidTarget(c, Character.Controlled, targetCharactersInOtherSubs: false));
ToggleReportButton("reportintruders", hasIntruders);
foreach (GUIComponent reportButton in ReportButtonFrame.Children)
@@ -3780,5 +3848,72 @@ namespace Barotrauma
GameMain.GameSession?.CrewManager?.AddOrder(order, fadeOutTime);
}
}
private class CharacterInfoComparer : IEqualityComparer<CharacterInfo>
{
public bool Equals(CharacterInfo x, CharacterInfo y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (x is null || y is null)
{
return false;
}
return x.ID == y.ID;
}
public int GetHashCode(CharacterInfo obj)
{
return obj.ID;
}
}
public bool UpdateReserveBenchIfNeeded(IEnumerable<CharacterInfo> updatedReserveBench)
{
var newBench = updatedReserveBench.ToHashSet(new CharacterInfoComparer());
var currentBench = reserveBench.ToHashSet(new CharacterInfoComparer());
bool updateNeeded = !newBench.SetEquals(currentBench);
if (updateNeeded)
{
reserveBench.Clear(); // since this is the reserve bench (characters not instantiated), there's no need to retain any references etc
reserveBench.AddRange(updatedReserveBench);
}
return updateNeeded;
}
/// <summary>
/// This will update which CharacterInfos should be in CrewManager and which shouldn't, excluding the reserve bench.
/// The CharacterInfos themselves aren't updated, they will only be either added, removed, or kept as-is.
/// </summary>
public bool UpdateCrewManagerIfNecessary(List<CharacterInfo> updatedCrewManager)
{
// CharacterInfos no longer in the server's CrewManager
var toRemove = characterInfos.Where(original => updatedCrewManager.None(updated => updated.ID == original.ID)).ToList();
// CharacterInfos that are in the server's CrewManager but not on the client yet
var toAdd = updatedCrewManager.Where(updated => characterInfos.None(original => original.ID == updated.ID)).ToList();
foreach (CharacterInfo characterInfo in toRemove)
{
if (characterInfo.Character is Character existingCharacter)
{
if (!existingCharacter.IsBot) { continue; } // on client side players are also stored here, we should skip those in this case
RemoveCharacter(characterInfo.Character, removeInfo: true, resetCrewListIndex: true);
}
else
{
characterInfos.Remove(characterInfo);
}
}
characterInfos.AddRange(toAdd);
return toRemove.Count > 0 || toAdd.Count > 0;
}
}
}

View File

@@ -366,7 +366,7 @@ namespace Barotrauma
default:
ShowCampaignUI = true;
CampaignUI.SelectTab(npc.CampaignInteractionType, npc);
CampaignUI.UpgradeStore?.RequestRefresh();
CampaignUI.UpgradeStore?.RequestRefresh(refreshUpgrades: true);
break;
}
@@ -395,13 +395,15 @@ namespace Barotrauma
protected void TryEndRoundWithFuelCheck(Action onConfirm, Action onReturnToMapScreen)
{
if (Submarine.MainSub == null) { return; }
Submarine.MainSub.CheckFuel();
bool lowFuel = Submarine.MainSub.Info.LowFuel;
if (PendingSubmarineSwitch != null)
{
lowFuel = TransferItemsOnSubSwitch ? (lowFuel && PendingSubmarineSwitch.LowFuel) : PendingSubmarineSwitch.LowFuel;
}
if (Level.IsLoadedFriendlyOutpost && lowFuel && CargoManager.PurchasedItems.None(i => i.Value.Any(pi => pi.ItemPrefab.Tags.Contains("reactorfuel"))))
if (Level.IsLoadedFriendlyOutpost && lowFuel && CargoManager.PurchasedItems.None(i => i.Value.Any(pi => pi.ItemPrefab.Tags.Contains(Tags.ReactorFuel))))
{
var extraConfirmationBox =
new GUIMessageBox(TextManager.Get("lowfuelheader"),

View File

@@ -532,6 +532,7 @@ namespace Barotrauma
bool isFirstRound = msg.ReadBoolean();
byte campaignID = msg.ReadByte();
byte roundId = msg.ReadByte();
UInt16 saveID = msg.ReadUInt16();
string mapSeed = msg.ReadString();
@@ -541,7 +542,7 @@ namespace Barotrauma
{
string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer);
GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty, mapSeed);
GameMain.GameSession = new GameSession(null, Option.None, CampaignDataPath.CreateRegular(savePath), GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty, mapSeed);
campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode;
campaign.CampaignID = campaignID;
GameMain.NetLobbyScreen.ToggleCampaignMode(true);
@@ -553,7 +554,7 @@ namespace Barotrauma
if (requiredFlags.HasFlag(NetFlags.Misc))
{
DebugConsole.Log("Received campaign update (Misc)");
DebugConsole.Log("Received campaign update (Misc), round id: " + roundId);
UInt16 id = msg.ReadUInt16();
bool purchasedHullRepairs = msg.ReadBoolean();
bool purchasedItemRepairs = msg.ReadBoolean();
@@ -571,7 +572,7 @@ namespace Barotrauma
if (requiredFlags.HasFlag(NetFlags.MapAndMissions))
{
DebugConsole.Log("Received campaign update (MapAndMissions)");
DebugConsole.Log("Received campaign update (MapAndMissions), round id: " + roundId);
UInt16 id = msg.ReadUInt16();
bool forceMapUI = msg.ReadBoolean();
bool allowDebugTeleport = msg.ReadBoolean();
@@ -634,7 +635,7 @@ namespace Barotrauma
if (requiredFlags.HasFlag(NetFlags.SubList))
{
DebugConsole.Log("Received campaign update (SubList)");
DebugConsole.Log("Received campaign update (SubList), round id: " + roundId);
UInt16 id = msg.ReadUInt16();
ushort ownedSubCount = msg.ReadUInt16();
List<ushort> ownedSubIndices = new List<ushort>();
@@ -679,7 +680,7 @@ namespace Barotrauma
if (requiredFlags.HasFlag(NetFlags.UpgradeManager))
{
DebugConsole.Log("Received campaign update (UpgradeManager)");
DebugConsole.Log("Received campaign update (UpgradeManager), round id: " + roundId);
UInt16 id = msg.ReadUInt16();
ushort pendingUpgradeCount = msg.ReadUInt16();
@@ -737,7 +738,7 @@ namespace Barotrauma
if (requiredFlags.HasFlag(NetFlags.ItemsInBuyCrate))
{
DebugConsole.Log("Received campaign update (ItemsInBuyCrate)");
DebugConsole.Log("Received campaign update (ItemsInBuyCrate), round id: " + roundId);
UInt16 id = msg.ReadUInt16();
var buyCrateItems = ReadPurchasedItems(msg, sender: null);
if (ShouldApply(NetFlags.ItemsInBuyCrate, id, requireUpToDateSave: true))
@@ -753,7 +754,7 @@ namespace Barotrauma
}
if (requiredFlags.HasFlag(NetFlags.ItemsInSellFromSubCrate))
{
DebugConsole.Log("Received campaign update (ItemsInSellFromSubCrate)");
DebugConsole.Log("Received campaign update (ItemsInSellFromSubCrate), round id: " + roundId);
UInt16 id = msg.ReadUInt16();
var subSellCrateItems = ReadPurchasedItems(msg, sender: null);
if (ShouldApply(NetFlags.ItemsInSellFromSubCrate, id, requireUpToDateSave: true))
@@ -769,7 +770,7 @@ namespace Barotrauma
}
if (requiredFlags.HasFlag(NetFlags.PurchasedItems))
{
DebugConsole.Log("Received campaign update (PuchasedItems)");
DebugConsole.Log("Received campaign update (PuchasedItems), round id: " + roundId);
UInt16 id = msg.ReadUInt16();
var purchasedItems = ReadPurchasedItems(msg, sender: null);
if (ShouldApply(NetFlags.PurchasedItems, id, requireUpToDateSave: true))
@@ -785,7 +786,7 @@ namespace Barotrauma
}
if (requiredFlags.HasFlag(NetFlags.SoldItems))
{
DebugConsole.Log("Received campaign update (SoldItems)");
DebugConsole.Log("Received campaign update (SoldItems), round id: " + roundId);
UInt16 id = msg.ReadUInt16();
var soldItems = ReadSoldItems(msg);
if (ShouldApply(NetFlags.SoldItems, id, requireUpToDateSave: true))
@@ -801,7 +802,7 @@ namespace Barotrauma
}
if (requiredFlags.HasFlag(NetFlags.Reputation))
{
DebugConsole.Log("Received campaign update (Reputation)");
DebugConsole.Log("Received campaign update (Reputation), round id: " + roundId);
UInt16 id = msg.ReadUInt16();
Dictionary<Identifier, float> factionReps = new Dictionary<Identifier, float>();
byte factionsCount = msg.ReadByte();
@@ -828,7 +829,7 @@ namespace Barotrauma
}
if (requiredFlags.HasFlag(NetFlags.CharacterInfo))
{
DebugConsole.Log("Received campaign update (CharacterInfo)");
DebugConsole.Log("Received campaign update (CharacterInfo), round id: " + roundId);
UInt16 id = msg.ReadUInt16();
bool hasCharacterData = msg.ReadBoolean();
CharacterInfo myCharacterInfo = null;
@@ -837,16 +838,27 @@ namespace Barotrauma
{
myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg, requireJobPrefabFound: !waitForModsDownloaded);
}
if (!waitForModsDownloaded && ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true))
//don't require the correct round ID for the character info if we're in the lobby
// = allow updating the character to the latest one in the lobby, even though we've not loaded to the same round as the server
if (!waitForModsDownloaded && ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true, requireCorrectRoundId: Screen.Selected != GameMain.NetLobbyScreen))
{
if (myCharacterInfo != null)
{
GameMain.Client.CharacterInfo = myCharacterInfo;
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(myCharacterInfo);
GameMain.GameSession.RefreshAnyOpenPlayerInfo();
}
else
{
//don't reset the character info nor the open UI here here,
//the client needs it to be able to customize the character they want to next spawn as
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
//if we've already discarded our current character and the server is just "verifying" that,
//no need to refresh the UI (no changes, refreshing would just throw the client out of the character settings panel)
if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
{
GameMain.GameSession.RefreshAnyOpenPlayerInfo();
}
}
}
}
@@ -863,8 +875,14 @@ namespace Barotrauma
}
campaign.SuppressStateSending = false;
bool ShouldApply(NetFlags flag, UInt16 id, bool requireUpToDateSave)
bool ShouldApply(NetFlags flag, UInt16 id, bool requireUpToDateSave, bool requireCorrectRoundId = true)
{
if (requireCorrectRoundId && roundId != campaign.RoundID)
{
DebugConsole.Log($"Received campaing update for a different round (client: {campaign.RoundID}, server: {roundId}), ignoring...");
return false;
}
if (NetIdUtils.IdMoreRecent(id, campaign.GetLastUpdateIdForFlag(flag)) &&
(!requireUpToDateSave || saveID == campaign.LastSaveID))
{
@@ -919,26 +937,42 @@ namespace Barotrauma
ushort pendingHireLength = msg.ReadUInt16();
List<UInt16> pendingHires = new List<UInt16>();
bool[] pendingHiresToReserveBench = new bool[pendingHireLength];
for (int i = 0; i < pendingHireLength; i++)
{
pendingHires.Add(msg.ReadUInt16());
pendingHiresToReserveBench[i] = msg.ReadBoolean();
}
ushort hiredLength = msg.ReadUInt16();
List<CharacterInfo> hiredCharacters = new List<CharacterInfo>();
for (int i = 0; i < hiredLength; i++)
List<CharacterInfo> updatedCrewManager = new List<CharacterInfo>();
ushort crewLength = msg.ReadUInt16();
for (int i = 0; i < crewLength; i++)
{
CharacterInfo hired = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg);
hired.Salary = msg.ReadInt32();
hiredCharacters.Add(hired);
CharacterInfo crewMember = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg);
if (crewMember.IsNewHire)
{
hiredCharacters.Add(crewMember);
}
updatedCrewManager.Add(crewMember);
}
bool crewManagerUpdated = GameMain.GameSession.CrewManager?.UpdateCrewManagerIfNecessary(updatedCrewManager) ?? false;
ushort reserveBenchLength = msg.ReadUInt16();
List<CharacterInfo> updatedReserveBench = new List<CharacterInfo>();
for (int i = 0; i < reserveBenchLength; i++)
{
CharacterInfo info = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg);
updatedReserveBench.Add(info);
}
bool reserveBenchUpdated = GameMain.GameSession.CrewManager?.UpdateReserveBenchIfNeeded(updatedReserveBench) ?? false;
bool renameCrewMember = msg.ReadBoolean();
if (renameCrewMember)
{
UInt16 renamedIdentifier = msg.ReadUInt16();
string newName = msg.ReadString();
CharacterInfo renamedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == renamedIdentifier);
CharacterInfo renamedCharacter = CrewManager.GetCharacterInfos(includeReserveBench: true).FirstOrDefault(info => info.ID == renamedIdentifier);
if (renamedCharacter != null)
{
CrewManager.RenameCharacter(renamedCharacter, newName);
@@ -955,7 +989,7 @@ namespace Barotrauma
if (fireCharacter)
{
UInt16 firedIdentifier = msg.ReadUInt16();
CharacterInfo firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == firedIdentifier);
CharacterInfo firedCharacter = CrewManager.GetCharacterInfos(includeReserveBench: true).FirstOrDefault(info => info.ID == firedIdentifier);
// this one might and is allowed to be null since the character is already fired on the original sender's game
if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); }
}
@@ -967,8 +1001,8 @@ namespace Barotrauma
{
CampaignUI.HRManagerUI.SetHireables(map.CurrentLocation, availableHires);
if (hiredCharacters.Any()) { CampaignUI.HRManagerUI.ValidateHires(hiredCharacters, takeMoney: false, createNotification: createNotification); }
CampaignUI.HRManagerUI.SetPendingHires(pendingHires, map.CurrentLocation);
if (renameCrewMember || fireCharacter) { CampaignUI.HRManagerUI.UpdateCrew(); }
//don't check the crew size limit: if the server says someone's hired, then it's so
CampaignUI.HRManagerUI.SetPendingHires(pendingHires, pendingHiresToReserveBench, map.CurrentLocation, checkCrewSizeLimit: false);
}
}
else
@@ -979,6 +1013,11 @@ namespace Barotrauma
CurrentLocation?.ForceHireableCharacters(availableHires);
}
if (fireCharacter || renameCrewMember || crewManagerUpdated || reserveBenchUpdated)
{
CampaignUI?.HRManagerUI?.RefreshHRView();
GameMain.GameSession?.DeathPrompt?.UpdateBotList();
}
}
public void ClientReadMoney(IReadMessage inc)
@@ -1044,7 +1083,7 @@ namespace Barotrauma
return false;
}
public override void Save(XElement element)
public override void Save(XElement element, bool isSavingOnLoading)
{
//do nothing, the clients get the save files from the server
}

View File

@@ -240,7 +240,7 @@ namespace Barotrauma
if (!savedOnStart)
{
GUI.SetSavingIndicatorState(true);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
SaveUtil.SaveGame(GameMain.GameSession.DataPath, isSavingOnLoading: true);
savedOnStart = true;
}
@@ -379,7 +379,7 @@ namespace Barotrauma
if (success)
{
// Event history must be registered before ending the round or it will be cleared
GameMain.GameSession.EventManager.RegisterEventHistory();
GameMain.GameSession.EventManager.StoreEventDataAtRoundEnd();
}
GameMain.GameSession.EndRound("", transitionType);
var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton;
@@ -448,7 +448,7 @@ namespace Barotrauma
if (success)
{
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
SaveUtil.SaveGame(GameMain.GameSession.DataPath);
}
else
{
@@ -479,7 +479,7 @@ namespace Barotrauma
protected override void EndCampaignProjSpecific()
{
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
SaveUtil.SaveGame(GameMain.GameSession.DataPath);
GameMain.CampaignEndScreen.Select();
GUI.DisableHUD = false;
GameMain.CampaignEndScreen.OnFinished = () =>
@@ -672,7 +672,7 @@ namespace Barotrauma
}
}
public override void Save(XElement element)
public override void Save(XElement element, bool isSavingOnLoading)
{
XElement modeElement = new XElement("SinglePlayerCampaign",
new XAttribute("purchasedlostshuttles", PurchasedLostShuttles),

View File

@@ -7,7 +7,7 @@ using Barotrauma.Items.Components;
namespace Barotrauma
{
class TestGameMode : GameMode
partial class TestGameMode : GameMode
{
public Action OnRoundEnd;
@@ -22,18 +22,6 @@ namespace Barotrauma
private GUIButton createEventButton;
public TestGameMode(GameModePreset preset) : base(preset)
{
foreach (JobPrefab jobPrefab in JobPrefab.Prefabs.OrderBy(p => p.Identifier))
{
for (int i = 0; i < jobPrefab.InitialCount; i++)
{
var variant = Rand.Range(0, jobPrefab.Variants);
CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant));
}
}
}
public override void Start()
{
base.Start();
@@ -42,14 +30,21 @@ namespace Barotrauma
foreach (Submarine submarine in Submarine.Loaded)
{
submarine.NeutralizeBallast();
//normally the body would be made static during level generation,
//but in the test mode we load the outpost/wreck/beacon as if it was a normal sub and need to do this manually
if (submarine.Info.Type == SubmarineType.Outpost ||
submarine.Info.Type == SubmarineType.OutpostModule ||
submarine.Info.Type == SubmarineType.Wreck ||
submarine.Info.Type == SubmarineType.BeaconStation)
switch (submarine.Info.Type)
{
submarine.PhysicsBody.BodyType = FarseerPhysics.BodyType.Static;
case SubmarineType.Outpost:
case SubmarineType.OutpostModule:
case SubmarineType.Wreck:
case SubmarineType.BeaconStation:
//normally the body would be made static during level generation,
//but in the test mode we load the outpost/wreck/beacon as if it was a normal sub and need to do this manually
submarine.PhysicsBody.BodyType = FarseerPhysics.BodyType.Static;
if (submarine.Info.ShouldBeRuin)
{
submarine.Info.Type = SubmarineType.Ruin;
}
submarine.TeamID = submarine.Info.IsOutpost ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None;
break;
}
}

View File

@@ -86,7 +86,7 @@ namespace Barotrauma.Tutorials
yield return CoroutineStatus.Running;
GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefabs: null);
GameMain.GameSession = new GameSession(subInfo, Option.None, GameModePreset.Tutorial, missionPrefabs: null);
(GameMain.GameSession.GameMode as TutorialMode).Tutorial = this;
if (generationParams is not null)
@@ -138,7 +138,7 @@ namespace Barotrauma.Tutorials
character = Character.Create(charInfo, wayPoint.WorldPosition, "", isRemotePlayer: false, hasAi: false);
character.TeamID = CharacterTeamType.Team1;
Character.Controlled = character;
character.GiveJobItems(null);
character.GiveJobItems(isPvPMode: false, null);
var idCard = character.Inventory.FindItemByTag("identitycard".ToIdentifier());
if (idCard == null)

View File

@@ -110,9 +110,13 @@ namespace Barotrauma
deathChoiceInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), parent: topLeftButtonGroup.RectTransform)
{ MaxSize = new Point(HUDLayoutSettings.ButtonAreaTop.Width / 3, int.MaxValue) }, style: null)
{
CanBeFocused = false,
Visible = false
};
respawnInfoText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), deathChoiceInfoFrame.RectTransform), "", wrap: true);
respawnInfoText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), deathChoiceInfoFrame.RectTransform), "", wrap: true)
{
CanBeFocused = false
};
deathChoiceButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), deathChoiceInfoFrame.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
AbsoluteSpacing = HUDLayoutSettings.Padding,
@@ -189,7 +193,7 @@ namespace Barotrauma
if (GameMain.NetworkMember != null)
{
GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu?.AddToGUIUpdateList();
GameMain.NetLobbyScreen?.JobSelectionFrame?.AddToGUIUpdateList();
GameMain.NetLobbyScreen?.JobSelectionFrame?.AddToGUIUpdateList(order: 1);
}
DeathPrompt?.AddToGUIUpdateList();
@@ -284,7 +288,7 @@ namespace Barotrauma
if (topLeftButtonGroup == null) { return; }
bool permadeathMode = GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath };
bool ironmanMode = GameMain.NetworkMember is { ServerSettings: { RespawnMode: RespawnMode.Permadeath, IronmanMode: true } };
bool ironmanMode = GameMain.NetworkMember?.ServerSettings is { IronmanModeActive: true };
bool hasRespawnOptions;
if (permadeathMode)
@@ -336,6 +340,20 @@ namespace Barotrauma
}
}
/// <summary>
/// If there are any menu panels etc. open that contain information about the current player character, refresh it.
/// Useful when the player character changes, e.g. at permadeath, and subsequent taking over of a bot character.
/// </summary>
public void RefreshAnyOpenPlayerInfo()
{
DebugConsole.NewMessage($"Refreshing any open player info");
if (IsTabMenuOpen && TabMenu.SelectedTab == TabMenu.InfoFrameTab.Talents)
{
TabMenuInstance.SelectInfoFrameTab(TabMenu.InfoFrameTab.Talents);
}
// TODO: This can be expanded as need arises
}
public void Draw(SpriteBatch spriteBatch)
{
GameMode?.Draw(spriteBatch);

View File

@@ -205,7 +205,7 @@ namespace Barotrauma
if (Character.Controlled.SelectedItem.GetComponent<Reactor>() is Reactor reactor && reactor.PowerOn &&
Character.Controlled.SelectedItem.OwnInventory?.AllItems is IEnumerable<Item> containedItems &&
containedItems.Count(i => i.HasTag(Tags.Fuel)) > 1)
containedItems.Count(i => i.HasTag(Tags.ReactorFuel)) > 1)
{
if (DisplayHint("onisinteracting.reactorwithextrarods".ToIdentifier())) { return; }
}
@@ -316,7 +316,7 @@ namespace Barotrauma
if (affliction?.Prefab == null) { continue; }
if (affliction.Prefab.IsBuff) { continue; }
if (affliction.Prefab == AfflictionPrefab.OxygenLow) { continue; }
if (affliction.Prefab == AfflictionPrefab.RadiationSickness && (GameMain.GameSession.Map?.Radiation?.IsEntityRadiated(character) ?? false)) { continue; }
if (affliction.Prefab == AfflictionPrefab.RadiationSickness && (GameMain.GameSession.Map?.Radiation?.DepthInRadiation(character) ?? 0) > 0) { continue; }
if (affliction.Strength < affliction.Prefab.ShowIconThreshold) { continue; }
DisplayHint("onafflictiondisplayed".ToIdentifier(),
variables: new[] { ("[key]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)) },

View File

@@ -0,0 +1,84 @@
using Microsoft.Xna.Framework;
namespace Barotrauma
{
partial class PvPMode : MissionMode
{
private GUIComponent scoreContainer;
private readonly GUITextBlock[] scoreTexts = new GUITextBlock[2];
private readonly GUITextBlock[] scoreTextShadows = new GUITextBlock[2];
private readonly int[] prevScores = new int[2];
private void InitUI()
{
scoreContainer = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), childAnchor: Anchor.TopRight)
{
CanBeFocused = false
};
for (int i = 0; i < 2; i++)
{
var frame = new GUIFrame(new RectTransform(new Point(scoreContainer.Rect.Width, GUI.IntScale(80)), scoreContainer.RectTransform), style: null)
{
CanBeFocused = false
};
new GUIImage(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight), style: i == 0 ? "CoalitionIcon" : "SeparatistIcon")
{
CanBeFocused = false
};
scoreTextShadows[i] = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(GUI.IntScale(38), GUI.IntScale(2)) },
string.Empty, textColor: GUIStyle.TextColorDark, textAlignment: Alignment.CenterRight, font: GUIStyle.SubHeadingFont)
{
CanBeFocused = false
};
scoreTexts[i] = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(GUI.IntScale(40), 0) },
string.Empty, textAlignment: Alignment.CenterRight, font: GUIStyle.SubHeadingFont)
{
CanBeFocused = false
};
}
}
public override void AddToGUIUpdateList()
{
base.AddToGUIUpdateList();
if (scoreContainer == null) { InitUI(); }
scoreContainer.Visible = false;
foreach (var mission in Missions)
{
if (mission is CombatMission combatMission && combatMission.HasWinScore)
{
for (int i = 0; i < 2; i++)
{
var scoreText = scoreTexts[i];
//one team very close to the win score, start flashing the score
if (combatMission.Scores[i] > combatMission.WinScore * 0.9f ||
combatMission.Scores[i] == combatMission.WinScore - 1)
{
if (scoreText.Parent.FlashTimer <= 0.0f)
{
scoreText.Parent.Flash(GUIStyle.Orange);
scoreText.Pulsate(Vector2.One, Vector2.One * 1.2f, scoreText.Parent.FlashTimer);
}
}
if (prevScores[i] != combatMission.Scores[i] || scoreText.Text.IsNullOrEmpty())
{
scoreText.Text = scoreTextShadows[i].Text = $"{combatMission.Scores[i]}/{combatMission.WinScore}";
scoreText.Parent.Flash(GUIStyle.Green);
scoreText.Parent.GetAnyChild<GUIImage>().Pulsate(Vector2.One, Vector2.One * 1.2f, scoreText.Parent.FlashTimer);
SoundPlayer.PlayUISound(GUISoundType.UIMessage);
}
scoreText.Parent.RectTransform.NonScaledSize =
new Point(
(int)(scoreText.TextSize.X + scoreText.Padding.X + scoreText.Padding.X) + scoreText.Parent.GetChild<GUIImage>().Rect.Width + GUI.IntScale(10),
scoreText.Parent.Rect.Height);
scoreText.Parent.ForceLayoutRecalculation();
prevScores[i] = combatMission.Scores[i];
}
scoreContainer.Visible = true;
}
}
scoreContainer.AddToGUIUpdateList();
}
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
@@ -13,11 +14,13 @@ namespace Barotrauma
private float crewListAnimDelay = 0.25f;
private float missionIconAnimDelay;
private const float jobColumnWidthPercentage = 0.11f;
private const float characterColumnWidthPercentage = 0.44f;
private const float statusColumnWidthPercentage = 0.45f;
private const float JobColumnWidthPercentage = 0.05f;
private const float CharacterColumnWidthPercentage = 0.35f;
private const float StatusColumnWidthPercentage = 0.25f;
private const float KillColumnWidthPercentage = 0.05f;
private const float DeathColumnWidthPercentage = 0.05f;
private int jobColumnWidth, characterColumnWidth, statusColumnWidth;
private int jobColumnWidth, characterColumnWidth, statusColumnWidth, killColumnWidth, deathColumnWidth;
private readonly List<Mission> selectedMissions;
private readonly Location startLocation, endLocation;
@@ -109,6 +112,16 @@ namespace Barotrauma
CombatMission.GetTeamName(CharacterTeamType.Team2), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont);
crewHeader2.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader2.Rect.Height * 2.0f));
CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == CharacterTeamType.Team2), traitorResults);
if (CombatMission.Winner != CharacterTeamType.None)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewHeader.RectTransform),
TextManager.Get(CombatMission.Winner == CharacterTeamType.Team1 ? "pvpmode.victory" : "pvpmode.defeat"), textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont,
textColor: CombatMission.Winner == CharacterTeamType.Team1 ? GUIStyle.Green : GUIStyle.Red);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewHeader2.RectTransform),
TextManager.Get(CombatMission.Winner == CharacterTeamType.Team2 ? "pvpmode.victory" : "pvpmode.defeat"), textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont,
textColor: CombatMission.Winner == CharacterTeamType.Team2 ? GUIStyle.Green : GUIStyle.Red);
}
}
//header -------------------------------------------------------------------------------
@@ -238,11 +251,12 @@ namespace Barotrauma
textContent,
mission.Difficulty ?? 0,
mission.Prefab.Icon, mission.Prefab.IconColor,
mission.GetDifficultyToolTipText(),
out GUIImage missionIcon);
if (selectedMissions.Contains(mission))
{
UpdateMissionStateIcon(mission.Completed, missionIcon, animDelay);
UpdateMissionStateIcon(mission is CombatMission combatMission ? CombatMission.IsInWinningTeam(GameMain.Client?.Character) : mission.Completed, missionIcon, animDelay);
animDelay += 0.25f;
}
}
@@ -429,13 +443,14 @@ namespace Barotrauma
textContent,
difficultyIconCount: 0,
icon, GUIStyle.Red,
difficultyTooltipText: null,
out GUIImage missionIcon);
UpdateMissionStateIcon(traitorResults.VotedCorrectTraitor, missionIcon, iconAnimDelay);
return content;
}
public static GUIComponent CreateMissionEntry(GUIComponent parent, LocalizedString header, List<LocalizedString> textContent, int difficultyIconCount,
Sprite icon, Color iconColor, out GUIImage missionIcon)
Sprite icon, Color iconColor, RichString difficultyTooltipText, out GUIImage missionIcon)
{
int spacing = GUI.IntScale(5);
@@ -486,7 +501,8 @@ namespace Barotrauma
{
difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(new Point(missionTextGroup.Rect.Width, defaultLineHeight), parent: missionTextGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
AbsoluteSpacing = 1
AbsoluteSpacing = 1,
CanBeFocused = true
};
difficultyIndicatorGroup.RectTransform.MinSize = new Point(0, defaultLineHeight);
var difficultyColor = Mission.GetDifficultyColor(difficultyIconCount);
@@ -494,8 +510,8 @@ namespace Barotrauma
{
new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), "DifficultyIndicator", scaleToFit: true)
{
CanBeFocused = false,
Color = difficultyColor
Color = difficultyColor,
ToolTip = difficultyTooltipText
};
}
}
@@ -567,7 +583,11 @@ namespace Barotrauma
LocalizedString locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.DisplayName : startLocation?.DisplayName;
string textTag;
if (gameOver)
if (gameMode is PvPMode)
{
textTag = "RoundSummaryRoundHasEnded";
}
else if (gameOver)
{
textTag = "RoundSummaryGameOver";
}
@@ -596,7 +616,14 @@ namespace Barotrauma
textTag = "RoundSummaryReturnToEmptyLocation";
break;
default:
textTag = Submarine.MainSub.AtEndExit ? "RoundSummaryProgress" : "RoundSummaryReturn";
if (Submarine.MainSub == null)
{
textTag = "RoundSummaryRoundHasEnded";
}
else
{
textTag = Submarine.MainSub.AtEndExit ? "RoundSummaryProgress" : "RoundSummaryReturn";
}
break;
}
}
@@ -628,26 +655,34 @@ namespace Barotrauma
{
var headerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform, Anchor.TopCenter, minSize: new Point(0, (int)(30 * GUI.Scale))) { }, isHorizontal: true)
{
AbsoluteSpacing = 2
AbsoluteSpacing = 2,
Stretch = true
};
GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale");
GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale");
GUIButton statusButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("label.statuslabel"), style: "GUIButtonSmallFreeScale");
GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(JobColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale");
GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(CharacterColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale");
float sizeMultiplier = 1.0f;
//sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width;
if (gameMode is PvPMode && GameMain.NetworkMember?.RespawnManager != null)
{
var killButton = new GUIButton(new RectTransform(new Vector2(KillColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("killcount"), style: "GUIButtonSmallFreeScale");
killColumnWidth = killButton.Rect.Width;
var deathButton = new GUIButton(new RectTransform(new Vector2(DeathColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("deathcount"), style: "GUIButtonSmallFreeScale");
deathColumnWidth = deathButton.Rect.Width;
}
else
{
GUIButton statusButton = new GUIButton(new RectTransform(new Vector2(StatusColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("label.statuslabel"), style: "GUIButtonSmallFreeScale");
statusColumnWidth = statusButton.Rect.Width;
}
jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f);
characterButton.RectTransform.RelativeSize = new Vector2(characterColumnWidthPercentage * sizeMultiplier, 1f);
statusButton.RectTransform.RelativeSize = new Vector2(statusColumnWidthPercentage * sizeMultiplier, 1f);
jobButton.TextBlock.Font = characterButton.TextBlock.Font = statusButton.TextBlock.Font = GUIStyle.HotkeyFont;
jobButton.CanBeFocused = characterButton.CanBeFocused = statusButton.CanBeFocused = false;
jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = statusButton.ForceUpperCase = ForceUpperCase.Yes;
foreach (var btn in headerFrame.GetAllChildren<GUIButton>())
{
btn.TextBlock.Font = GUIStyle.HotkeyFont;
btn.ForceUpperCase = ForceUpperCase.Yes;
btn.CanBeFocused = false;
}
jobColumnWidth = jobButton.Rect.Width;
characterColumnWidth = characterButton.Rect.Width;
statusColumnWidth = statusButton.Rect.Width;
GUIListBox crewList = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform))
{
@@ -658,8 +693,28 @@ namespace Barotrauma
headerFrame.RectTransform.RelativeSize -= new Vector2(crewList.ScrollBar.RectTransform.RelativeSize.X, 0.0f);
killCounts.Clear();
if (GameMain.NetworkMember != null)
{
foreach (CharacterInfo characterInfo in characterInfos)
{
if (characterInfo == null) { continue; }
Character character = characterInfo.Character;
Client ownerClient = GameMain.NetworkMember.ConnectedClients.FirstOrDefault(c => c.Character == character);
int killCount = 0, deathCount = 0;
foreach (var mission in selectedMissions)
{
if (mission is not CombatMission combatMission) { continue; }
killCount += ownerClient == null ? combatMission.GetBotKillCount(characterInfo) : combatMission.GetClientKillCount(ownerClient);
deathCount += ownerClient == null ? combatMission.GetBotDeathCount(characterInfo) : combatMission.GetClientDeathCount(ownerClient);
}
killCounts[characterInfo] = killCount;
deathCounts[characterInfo] = deathCount;
}
}
float delay = crewListAnimDelay;
foreach (CharacterInfo characterInfo in characterInfos)
foreach (CharacterInfo characterInfo in characterInfos.OrderByDescending(ci => killCounts.GetValueOrDefault(ci)))
{
if (characterInfo == null) { continue; }
CreateCharacterElement(characterInfo, crewList, traitorResults, delay);
@@ -670,6 +725,10 @@ namespace Barotrauma
return crewList;
}
private readonly Dictionary<CharacterInfo, int> killCounts = new();
private readonly Dictionary<CharacterInfo, int> deathCounts = new();
private void CreateCharacterElement(CharacterInfo characterInfo, GUIListBox listBox, TraitorManager.TraitorResults? traitorResults, float animDelay)
{
GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, GUI.IntScale(45)), listBox.Content.RectTransform), style: "ListBoxElement")
@@ -701,7 +760,7 @@ namespace Barotrauma
Character character = characterInfo.Character;
if (character == null || character.IsDead)
{
if (character == null && characterInfo.IsNewHire && characterInfo.CauseOfDeath == null)
if (character == null && (characterInfo.IsNewHire || characterInfo.BotStatus == BotStatus.ActiveService) && characterInfo.CauseOfDeath == null)
{
statusText = TextManager.Get("CampaignCrew.NewHire");
statusColor = GUIStyle.Blue;
@@ -741,8 +800,21 @@ namespace Barotrauma
}
}
GUITextBlock statusBlock = new GUITextBlock(new RectTransform(new Point(statusColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
ToolBox.LimitString(statusText.Value, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: statusColor);
if (gameMode is PvPMode && GameMain.NetworkMember?.RespawnManager != null)
{
new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
killCounts.GetValueOrDefault(characterInfo).ToString(), textAlignment: Alignment.Center);
new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
deathCounts.GetValueOrDefault(characterInfo).ToString(), textAlignment: Alignment.Center);
}
else
{
GUITextBlock statusBlock = new GUITextBlock(new RectTransform(new Point(statusColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
ToolBox.LimitString(statusText.Value, GUIStyle.SmallFont, statusColumnWidth), textAlignment: Alignment.Center, textColor: statusColor, font: GUIStyle.SmallFont)
{
ToolTip = statusText.Value
};
}
frame.FadeIn(animDelay, 0.15f);
foreach (var child in frame.GetAllChildren())

View File

@@ -84,7 +84,7 @@ namespace Barotrauma
get { return layout; }
set
{
if (layout == value) return;
if (layout == value) { return; }
layout = value;
SetSlotPositions(layout);
}
@@ -259,8 +259,8 @@ namespace Barotrauma
int spacing = GUI.IntScale(5);
SlotSize = (SlotSpriteSmall.size * UIScale * GUI.AspectRatioAdjustment).ToPoint();
int bottomOffset = SlotSize.Y + spacing * 2 + ContainedIndicatorHeight;
int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - spacing * 2 - (int)(UnequippedIndicator.size.Y * UIScale);
int bottomOffset = GetBottomOffset(multiplier: 2);
int personalSlotY = GetVerticalOffsetFromBottom(multiplier: 2);
if (visualSlots == null) { CreateSlots(); }
if (visualSlots.None()) { return; }
@@ -353,7 +353,15 @@ namespace Barotrauma
case Layout.Left:
{
int x = HUDLayoutSettings.InventoryAreaLower.X;
if (!GUI.IsUltrawide && GUI.IsHUDScaled)
{
// On non-ultra-wide aspect ratios, the inventories can easily overlap with each other, if there's any scaling.
// So let's offset the other inventory to the left.
const float margin = 100;
x -= HUDLayoutSettings.ChatBoxArea.Width - (int)margin;
}
int personalSlotX = x;
float y = GameMain.GraphicsHeight - bottomOffset;
for (int i = 0; i < SlotPositions.Length; i++)
{
@@ -366,7 +374,7 @@ namespace Barotrauma
}
else
{
SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset);
SlotPositions[i] = new Vector2(x, y);
x += visualSlots[i].Rect.Width + spacing;
}
}
@@ -380,7 +388,7 @@ namespace Barotrauma
continue;
}
if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; }
SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset);
SlotPositions[i] = new Vector2(x, y);
x += visualSlots[i].Rect.Width + spacing;
}
}
@@ -446,6 +454,9 @@ namespace Barotrauma
visualSlots[i].DrawOffset = Vector2.Zero;
}
}
int GetBottomOffset(int multiplier) => SlotSize.Y + spacing * multiplier + ContainedIndicatorHeight;
int GetVerticalOffsetFromBottom(int multiplier) => GameMain.GraphicsHeight - (GetBottomOffset(multiplier) + spacing) * multiplier - (int)(UnequippedIndicator.size.Y * UIScale);
}
protected override void ControlInput(Camera cam)

View File

@@ -184,6 +184,13 @@ namespace Barotrauma.Items.Components
{
shakePos = Vector2.Zero;
}
if (Character.Controlled is Character character && character.FocusedItem == item)
{
if ((IsFullyOpen || IsFullyClosed) && MathF.Abs(openState - lastOpenState) > 0)
{
CharacterHUD.RecreateHudTexts = true;
}
}
}
public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null)

View File

@@ -14,6 +14,7 @@ namespace Barotrauma.Items.Components
public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description)
{
bool mergedMaterialTainted = false;
if (!materialName.IsNullOrEmpty() && item.ContainedItems.Count() > 0)
{
LocalizedString mergedMaterialName = materialName;
@@ -22,11 +23,15 @@ namespace Barotrauma.Items.Components
var containedMaterial = containedItem.GetComponent<GeneticMaterial>();
if (containedMaterial == null) { continue; }
mergedMaterialName += ", " + containedMaterial.materialName;
if (containedMaterial.Tainted)
{
mergedMaterialTainted = true;
}
}
name = name.Replace(materialName, mergedMaterialName);
}
if (Tainted)
if (Tainted || mergedMaterialTainted)
{
name = TextManager.GetWithVariable("entityname.taintedgeneticmaterial", "[geneticmaterialname]", name);
}
@@ -48,25 +53,46 @@ namespace Barotrauma.Items.Components
description += '\n' + containedDescription;
}
}
if (GameMain.DevMode && Tainted && selectedTaintedEffect != null)
{
description = $"{description}\n{selectedTaintedEffect.Name}: {selectedTaintedEffect.GetDescription(0f, AfflictionPrefab.Description.TargetType.OtherCharacter)}";
}
}
public void ModifyDeconstructInfo(Deconstructor deconstructor, ref LocalizedString buttonText, ref LocalizedString infoText)
{
if (deconstructor.InputContainer.Inventory.AllItems.Count() == 2)
{
var otherGeneticMaterial =
deconstructor.InputContainer.Inventory.AllItems.FirstOrDefault(it => it != item && it.Prefab == item.Prefab)?.GetComponent<GeneticMaterial>();
var otherItem = deconstructor.InputContainer.Inventory.AllItems.FirstOrDefault(it => it != item);
if (otherItem == null)
{
return;
}
var otherGeneticMaterial = otherItem.GetComponent<GeneticMaterial>();
if (otherGeneticMaterial == null)
{
buttonText = TextManager.Get("researchstation.combine");
infoText = TextManager.Get("researchstation.combine.infotext");
return;
}
else
var combineRefineResult = GetCombineRefineResult(otherGeneticMaterial);
if (combineRefineResult == CombineResult.None)
{
infoText = TextManager.Get("researchstation.novalidcombination");
}
else if (combineRefineResult == CombineResult.Refined)
{
buttonText = TextManager.Get("researchstation.refine");
int taintedProbability = (int)(GetTaintedProbabilityOnRefine(otherGeneticMaterial, Character.Controlled) * 100);
infoText = TextManager.GetWithVariable("researchstation.refine.infotext", "[taintedprobability]", taintedProbability.ToString());
}
else
{
buttonText = TextManager.Get("researchstation.combine");
infoText = TextManager.Get("researchstation.combine.infotext");
}
}
}

View File

@@ -71,13 +71,13 @@ namespace Barotrauma.Items.Components
}
public override bool ValidateEventData(NetEntityEvent.IData data)
=> TryExtractEventData<EventData>(data, out _);
=> TryExtractEventData<AttachEventData>(data, out _);
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
{
if (!attachable || body == null) { return; }
var eventData = ExtractEventData<EventData>(extraData);
var eventData = ExtractEventData<AttachEventData>(extraData);
Vector2 attachPos = eventData.AttachPos;
msg.WriteSingle(attachPos.X);
@@ -94,7 +94,9 @@ namespace Barotrauma.Items.Components
bool shouldBeAttached = msg.ReadBoolean();
Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle());
UInt16 submarineID = msg.ReadUInt16();
UInt16 attacherID = msg.ReadUInt16();
Submarine sub = Entity.FindEntityByID(submarineID) as Submarine;
Character attacher = Entity.FindEntityByID(attacherID) as Character;
if (shouldBeAttached)
{
@@ -104,6 +106,8 @@ namespace Barotrauma.Items.Components
item.SetTransform(simPosition, 0.0f);
item.Submarine = sub;
AttachToWall();
PlaySound(ActionType.OnUse, attacher);
ApplyStatusEffects(ActionType.OnUse, (float)Timing.Step, character: attacher, user: attacher);
}
}
else

View File

@@ -34,6 +34,8 @@ namespace Barotrauma.Items.Components
}
private Vector2 _chargeSoundWindupPitchSlide;
public Vector2 BarrelScreenPos => Screen.Selected.Cam.WorldToScreen(item.DrawPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos));
private readonly List<ParticleEmitter> particleEmitters = new List<ParticleEmitter>();
private readonly List<ParticleEmitter> particleEmitterCharges = new List<ParticleEmitter>();
@@ -86,28 +88,19 @@ namespace Barotrauma.Items.Components
currentCrossHairScale = currentCrossHairPointerScale = cam == null ? 1.0f : cam.Zoom;
if (crosshairSprite != null)
{
Vector2 aimRefWorldPos = character.AimRefPosition;
if (character.Submarine != null) { aimRefWorldPos += character.Submarine.Position; }
Vector2 itemPos = cam.WorldToScreen(aimRefWorldPos);
float rotation = (item.body.Dir == 1.0f) ? item.body.Rotation : item.body.Rotation - MathHelper.Pi;
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
Vector2 mouseDiff = itemPos - PlayerInput.MousePosition;
crosshairPos = new Vector2(
MathHelper.Clamp(itemPos.X + barrelDir.X * mouseDiff.Length(), 0, GameMain.GraphicsWidth),
MathHelper.Clamp(itemPos.Y + barrelDir.Y * mouseDiff.Length(), 0, GameMain.GraphicsHeight));
// Set position based on in-world aim
Vector2 barrelDir = (MathF.Cos(item.body.TransformedRotation), -MathF.Sin(item.body.TransformedRotation));
float mouseDist = Vector2.Distance(BarrelScreenPos, PlayerInput.MousePosition);
crosshairPos = Vector2.Clamp(BarrelScreenPos + barrelDir * mouseDist, Vector2.Zero, (GameMain.GraphicsWidth, GameMain.GraphicsHeight));
// Resize pointer based on current spread
float spread = GetSpread(character);
Projectile projectile = FindProjectile();
if (projectile != null)
if (FindProjectile() is Projectile projectile)
{
spread += MathHelper.ToRadians(projectile.Spread);
}
float crossHairDist = Vector2.Distance(item.WorldPosition, cam.ScreenToWorld(crosshairPos));
float spreadDist = (float)Math.Sin(spread) * crossHairDist;
currentCrossHairPointerScale = MathHelper.Clamp(spreadDist / Math.Min(crosshairSprite.size.X, crosshairSprite.size.Y), 0.1f, 10.0f);
float spreadAtRange = MathF.Sin(spread) * Vector2.Distance(BarrelScreenPos, crosshairPos);
currentCrossHairPointerScale = MathHelper.Clamp(spreadAtRange / Math.Min(crosshairSprite.size.X, crosshairSprite.size.Y), 0.1f, 10f);
}
currentCrossHairScale *= CrossHairScale;
crosshairPointerPos = PlayerInput.MousePosition;
@@ -141,7 +134,7 @@ namespace Barotrauma.Items.Components
{
if (chargeSound != null)
{
chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling, freqMult: chargeSound.GetRandomFrequencyMultiplier());
chargeSoundChannel = SoundPlayer.PlaySound(chargeSound, item.WorldPosition, hullGuess: item.CurrentHull);
if (chargeSoundChannel != null) { chargeSoundChannel.Looping = true; }
}
}
@@ -177,6 +170,8 @@ namespace Barotrauma.Items.Components
//don't draw the crosshair if the item is in some other type of equip slot than hands (e.g. assault rifle in the bag slot)
if (!character.HeldItems.Contains(item)) { return; }
base.DrawHUD(spriteBatch, character);
GUI.HideCursor = (crosshairSprite != null || crosshairPointerSprite != null) &&
GUI.MouseOn == null && !Inventory.IsMouseOnInventory && !GameMain.Instance.Paused;

View File

@@ -216,6 +216,7 @@ namespace Barotrauma.Items.Components
public override void DrawHUD(SpriteBatch spriteBatch, Character character)
{
if (character == null || !character.IsKeyDown(InputType.Aim)) { return; }
base.DrawHUD(spriteBatch, character);
GUI.HideCursor = targetSections.Count > 0;
}

View File

@@ -62,6 +62,10 @@ namespace Barotrauma.Items.Components
private readonly Dictionary<ActionType, List<ItemSound>> sounds;
private Dictionary<ActionType, SoundSelectionMode> soundSelectionModes;
/// <summary>
/// Starts the timer for delayed client-side corrections (<see cref="StartDelayedCorrection(IReadMessage, float, bool)"/>) - in other words,
/// the client will not attempt to read server updates for this component until the timer elapses.
/// </summary>
protected float correctionTimer;
public float IsActiveTimer;
@@ -150,6 +154,17 @@ namespace Barotrauma.Items.Components
public GUIFrame GuiFrame { get; set; }
/// <summary>
/// Overlay (just a non-interactable sprite) drawn when the item is selected, equipped or focused to via Controllers (e.g. when operating a turret via a periscope or a camera via a monitor).
/// </summary>
public Sprite HUDOverlay { get; set; }
public float HUDOverlayAnimSpeed
{
get;
set;
}
private GUIDragHandle guiFrameDragHandle;
private bool guiFrameUpdatePending;
@@ -161,6 +176,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No)]
public bool CloseByClickingOutsideGUIFrame
{
get;
set;
}
private ItemComponent linkToUIComponent;
[Serialize("", IsPropertySaveable.No)]
public string LinkUIToComponent
@@ -261,7 +283,7 @@ namespace Barotrauma.Items.Components
if (GameMain.Client?.MidRoundSyncing ?? false) { return; }
//above the top boundary of the level (in an inactive respawn shuttle?)
if (item.Submarine != null && Level.Loaded != null && item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y)
if (item.Submarine != null && item.Submarine.IsAboveLevel)
{
return;
}
@@ -294,11 +316,13 @@ namespace Barotrauma.Items.Components
0.01f,
loopingSound.RoundSound.GetRandomFrequencyMultiplier(),
SoundPlayer.ShouldMuffleSound(Character.Controlled, item.WorldPosition, loopingSound.Range, Character.Controlled?.CurrentHull));
loopingSoundChannel.Looping = true;
item.CheckNeedsSoundUpdate(this);
//TODO: tweak
loopingSoundChannel.Near = loopingSound.Range * 0.4f;
loopingSoundChannel.Far = loopingSound.Range;
if (loopingSoundChannel != null)
{
loopingSoundChannel.Looping = true;
item.CheckNeedsSoundUpdate(this);
loopingSoundChannel.Near = loopingSound.Range * 0.4f;
loopingSoundChannel.Far = loopingSound.Range;
}
}
// Looping sound with manual selection mode should be changed if value of ManuallySelectedSound has changed
@@ -340,7 +364,7 @@ namespace Barotrauma.Items.Components
}
else if (soundSelectionMode == SoundSelectionMode.Manual)
{
index = Math.Clamp(ManuallySelectedSound, 0, matchingSounds.Count);
index = Math.Clamp(ManuallySelectedSound, 0, matchingSounds.Count - 1);
}
else
{
@@ -374,22 +398,20 @@ namespace Barotrauma.Items.Components
float volume = GetSoundVolume(itemSound);
if (volume <= 0.0001f) { return; }
loopingSound = itemSound;
loopingSoundChannel = loopingSound.RoundSound.Sound.Play(
new Vector3(position.X, position.Y, 0.0f),
0.01f,
freqMult: itemSound.RoundSound.GetRandomFrequencyMultiplier(),
muffle: SoundPlayer.ShouldMuffleSound(Character.Controlled, position, loopingSound.Range, Character.Controlled?.CurrentHull));
loopingSoundChannel.Looping = true;
//TODO: tweak
loopingSoundChannel.Near = loopingSound.Range * 0.4f;
loopingSoundChannel.Far = loopingSound.Range;
loopingSoundChannel = SoundPlayer.PlaySound(loopingSound.RoundSound, position, volume: 0.01f, hullGuess: item.CurrentHull);
if (loopingSoundChannel != null)
{
loopingSoundChannel.Looping = true;
loopingSoundChannel.Near = loopingSound.Range * 0.4f;
loopingSoundChannel.Far = loopingSound.Range;
}
}
}
else
{
float volume = GetSoundVolume(itemSound);
if (volume <= 0.0001f) { return; }
var channel = SoundPlayer.PlaySound(itemSound.RoundSound.Sound, position, volume, itemSound.Range, itemSound.RoundSound.GetRandomFrequencyMultiplier(), item.CurrentHull, ignoreMuffling: itemSound.RoundSound.IgnoreMuffling);
var channel = SoundPlayer.PlaySound(itemSound.RoundSound, position, volume, hullGuess: item.CurrentHull);
if (channel != null) { playingOneshotSoundChannels.Add(channel); }
}
}
@@ -416,12 +438,23 @@ namespace Barotrauma.Items.Components
if (sound == null) { return 0.0f; }
if (sound.VolumeProperty == "") { return sound.VolumeMultiplier; }
if (SerializableProperties.TryGetValue(sound.VolumeProperty, out SerializableProperty property))
SerializableProperty property = null;
ISerializableEntity targetEntity = null;
if (SerializableProperties.TryGetValue(sound.VolumeProperty, out property))
{
targetEntity = this;
}
else if (Item.SerializableProperties.TryGetValue(sound.VolumeProperty, out property))
{
targetEntity = Item;
}
if (property != null)
{
float newVolume;
try
{
newVolume = property.GetFloatValue(this);
newVolume = property.GetFloatValue(targetEntity);
}
catch
{
@@ -470,7 +503,24 @@ namespace Barotrauma.Items.Components
return linkToUIComponent;
}
public virtual void DrawHUD(SpriteBatch spriteBatch, Character character) { }
public virtual void DrawHUD(SpriteBatch spriteBatch, Character character)
{
if (HUDOverlay != null)
{
Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
if (HUDOverlay is SpriteSheet spriteSheet)
{
spriteSheet.Draw(spriteBatch,
spriteIndex: (int)(Math.Floor(Timing.TotalTimeUnpaused * HUDOverlayAnimSpeed) % spriteSheet.FrameCount),
pos: screenSize / 2, color: Color.White, origin: HUDOverlay.Origin, rotate: 0, scale: screenSize / spriteSheet.FrameSize.ToVector2());
}
else
{
HUDOverlay.Draw(spriteBatch,
pos: screenSize / 2, color: Color.White, origin: HUDOverlay.Origin, rotate: 0, scale: screenSize / HUDOverlay.size);
}
}
}
public virtual void AddToGUIUpdateList(int order = 0)
{
@@ -513,6 +563,13 @@ namespace Barotrauma.Items.Components
GuiFrameSource = subElement;
ReloadGuiFrame();
break;
case "hudoverlayanimated":
HUDOverlay = new SpriteSheet(subElement);
HUDOverlayAnimSpeed = subElement.GetAttributeFloat("animspeed", 1.0f);
break;
case "hudoverlay":
HUDOverlay = new Sprite(subElement);
break;
case "alternativelayout":
AlternativeLayout = GUILayoutSettings.Load(subElement);
break;
@@ -687,6 +744,9 @@ namespace Barotrauma.Items.Components
}),
new ContextMenuOption(TextManager.Get(LockGuiFramePosition ? "item.unlockuiposition" : "item.lockuiposition"), isEnabled: true, onSelected: () =>
{
//ensure the offset is set to where the frame is now
//(it may have been repositioned by the overlap prevention logic, which doesn't set this offset)
GuiFrameOffset = GuiFrame.RectTransform.ScreenSpaceOffset;
LockGuiFramePosition = !LockGuiFramePosition;
guiFrameDragHandle.Enabled = !LockGuiFramePosition;
if (SerializableProperties.TryGetValue(nameof(LockGuiFramePosition).ToIdentifier(), out var property))
@@ -711,7 +771,11 @@ namespace Barotrauma.Items.Components
/// </summary>
protected virtual void CreateGUI() { }
//Starts a coroutine that will read the correct state of the component from the NetBuffer when correctionTimer reaches zero.
/// <summary>
/// Starts a coroutine that will read the correct state of the component from the NetBuffer when correctionTimer reaches zero.
/// Useful in cases where we a client is constantly adjusting some value, and we don't want state updates from the server to interfere with it
/// (e.g. setting the value back to what a client just set it to, when the client has already modified the value further).
/// </summary>
protected void StartDelayedCorrection(IReadMessage buffer, float sendingTime, bool waitForMidRoundSync = false)
{
if (delayedCorrectionCoroutine != null) { CoroutineManager.StopCoroutines(delayedCorrectionCoroutine); }

View File

@@ -458,54 +458,14 @@ namespace Barotrauma.Items.Components
public void DrawContainedItems(SpriteBatch spriteBatch, float itemDepth, Color? overrideColor = null)
{
Vector2 transformedItemPos = ItemPos * item.Scale;
Vector2 transformedItemInterval = ItemInterval * item.Scale;
Vector2 transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f);
Vector2 transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y);
var rootBody = item.RootContainer?.body ?? item.body;
if (item.body == null)
{
if (item.FlippedX)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemPos.X += item.Rect.Width;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
if (item.FlippedY)
{
transformedItemPos.Y = -transformedItemPos.Y;
transformedItemPos.Y -= item.Rect.Height;
transformedItemInterval.Y = -transformedItemInterval.Y;
transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y;
}
transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y);
if (item.Submarine != null) { transformedItemPos += item.Submarine.DrawPosition; }
if (Math.Abs(item.RotationRad) > 0.01f)
{
Matrix transform = Matrix.CreateRotationZ(-item.RotationRad);
transformedItemPos = Vector2.Transform(transformedItemPos - item.DrawPosition, transform) + item.DrawPosition;
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
}
}
else
{
Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation);
if (item.body.Dir == -1.0f)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
transformedItemPos = Vector2.Transform(transformedItemPos, transform);
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemPos += item.body.DrawPosition;
}
Vector2 transformedItemPos = GetContainedPosition(
drawPosition: true,
out Vector2 transformedItemIntervalHorizontal,
out Vector2 transformedItemIntervalVertical,
out bool flippedX,
out bool flippedY);
Vector2 currentItemPos = transformedItemPos;
@@ -514,30 +474,41 @@ namespace Barotrauma.Items.Components
int i = 0;
foreach (ContainedItem contained in containedItems)
{
Vector2 itemPos = currentItemPos;
if (contained.Item?.Sprite == null) { continue; }
if (contained.Hide) { continue; }
Vector2 itemPos = transformedItemPos;
int targetSlotIndex = ItemsUseInventoryPlacement ? Inventory.FindIndex(contained.Item) : i;
//interval set on both axes -> use a grid layout
if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f)
{
itemPos += transformedItemIntervalHorizontal * (targetSlotIndex % ItemsPerRow);
itemPos += transformedItemIntervalVertical * (targetSlotIndex / ItemsPerRow);
}
else
{
itemPos += (transformedItemIntervalHorizontal + transformedItemIntervalVertical) * targetSlotIndex;
}
if (contained.ItemPos.HasValue)
{
Vector2 pos = contained.ItemPos.Value;
if (item.body != null)
{
Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation);
pos.X *= item.body.Dir;
pos.X *= rootBody.Dir;
itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition;
}
else
{
itemPos = pos;
// This code is aped based on above. Not tested.
if (item.FlippedX)
if (flippedX)
{
itemPos.X = -itemPos.X;
itemPos.X += item.Rect.Width;
}
if (item.FlippedY)
if (flippedY)
{
itemPos.Y = -itemPos.Y;
itemPos.Y -= item.Rect.Height;
@@ -555,15 +526,15 @@ namespace Barotrauma.Items.Components
}
}
if (AutoInteractWithContained)
if (CanAutoInteractWithContained(contained.Item) && Screen.Selected is not { IsEditor: true })
{
contained.Item.IsHighlighted = item.IsHighlighted;
item.IsHighlighted = false;
}
Vector2 origin = contained.Item.Sprite.Origin;
if (item.FlippedX) { origin.X = contained.Item.Sprite.SourceRect.Width - origin.X; }
if (item.FlippedY) { origin.Y = contained.Item.Sprite.SourceRect.Height - origin.Y; }
if (flippedX) { origin.X = contained.Item.Sprite.SourceRect.Width - origin.X; }
if (flippedY) { origin.Y = contained.Item.Sprite.SourceRect.Height - origin.Y; }
float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? contained.Item.Sprite.Depth : ContainedSpriteDepth;
if (i < containedSpriteDepths.Length)
@@ -578,12 +549,13 @@ namespace Barotrauma.Items.Components
{
spriteRotation = contained.Rotation;
}
bool flipX = (item.body != null && item.body.Dir == -1) || item.FlippedX;
bool flipX = rootBody is { Dir: -1 } || flippedX;
if (flipX)
{
spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally;
}
bool flipY = item.FlippedY;
bool flipY = flippedY;
if (flipY)
{
spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically;
@@ -598,7 +570,8 @@ namespace Barotrauma.Items.Components
contained.Item.Scale,
spriteEffects,
depth: containedSpriteDepth);
contained.Item.DrawDecorativeSprites(spriteBatch, itemPos, flipX,flipY, (contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation),
contained.Item.DrawDecorativeSprites(spriteBatch, itemPos, flipX, flipY, (contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation),
containedSpriteDepth, overrideColor);
foreach (ItemContainer ic in contained.Item.GetComponents<ItemContainer>())
@@ -608,20 +581,6 @@ namespace Barotrauma.Items.Components
}
i++;
if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f)
{
//interval set on both axes -> use a grid layout
currentItemPos += transformedItemIntervalHorizontal;
if (i % ItemsPerRow == 0)
{
currentItemPos = transformedItemPos;
currentItemPos += transformedItemIntervalVertical * (i / ItemsPerRow);
}
}
else
{
currentItemPos += transformedItemInterval;
}
}
}

View File

@@ -41,7 +41,7 @@ namespace Barotrauma.Items.Components
}
private string text;
[Serialize("", IsPropertySaveable.Yes, translationTextTag: "Label.", description: "The text displayed in the label.", alwaysUseInstanceValues: true), Editable(100)]
[Serialize("", IsPropertySaveable.Yes, translationTextTag: "Label.", description: "The text displayed in the label.", alwaysUseInstanceValues: true), Editable(MaxLength = 100)]
public string Text
{
get { return text; }

View File

@@ -35,6 +35,7 @@ namespace Barotrauma.Items.Components
{
Light.SpriteScale = Vector2.One * item.Scale;
Light.Position = ParentBody != null ? ParentBody.Position : item.Position;
SetLightSourceTransformProjSpecific();
}
partial void SetLightSourceState(bool enabled, float brightness)
@@ -51,27 +52,42 @@ namespace Barotrauma.Items.Components
partial void SetLightSourceTransformProjSpecific()
{
Vector2 offset = Vector2.Zero;
if (LightOffset != Vector2.Zero)
{
offset = Vector2.Transform(LightOffset, Matrix.CreateRotationZ(item.FlippedY ? -item.RotationRad - MathHelper.Pi : -item.RotationRad)) * item.Scale;
}
if (ParentBody != null)
{
Light.ParentBody = ParentBody;
Light.OffsetFromBody = offset;
}
else if (turret != null)
{
Light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y);
Light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y) + offset;
}
else if (item.body != null)
{
Light.ParentBody = item.body;
Light.OffsetFromBody = offset;
}
else
{
Light.Position = item.Position;
Light.Position = item.Position + offset;
}
PhysicsBody body = Light.ParentBody;
if (body != null && body.Enabled)
if (body != null)
{
Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi;
Light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically;
if (body.Enabled)
{
Light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically;
}
else
{
Light.LightSpriteEffect = item.SpriteEffects;
}
}
else
{
@@ -85,11 +101,13 @@ namespace Barotrauma.Items.Components
if (Light?.LightSprite == null) { return; }
if ((item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled)
{
Vector2 offset = Vector2.Transform(LightOffset, Matrix.CreateRotationZ(item.FlippedY ? -item.RotationRad - MathHelper.Pi : -item.RotationRad)) * item.Scale;
Vector2 origin = Light.LightSprite.Origin;
if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; }
if ((Light.LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = Light.LightSprite.SourceRect.Height - origin.Y; }
Vector2 drawPos = item.body?.DrawPosition ?? item.DrawPosition;
Vector2 drawPos = item.body?.DrawPosition ?? item.DrawPosition + offset;
Color color = lightColor;
if (Light.OverrideLightSpriteAlpha.HasValue)

View File

@@ -6,11 +6,11 @@ namespace Barotrauma.Items.Components
{
partial class Controller : ItemComponent
{
private bool chatBoxOriginalState;
private bool isHUDsHidden;
public override void DrawHUD(SpriteBatch spriteBatch, Character character)
{
base.DrawHUD(spriteBatch, character);
if (focusTarget != null && character.ViewTarget == focusTarget)
{
foreach (ItemComponent ic in focusTarget.Components)
@@ -23,48 +23,31 @@ namespace Barotrauma.Items.Components
}
}
public override void AddToGUIUpdateList(int order = 0)
{
base.AddToGUIUpdateList(order);
if (focusTarget != null && Character.Controlled.ViewTarget == focusTarget)
{
focusTarget.AddToGUIUpdateList(order);
}
}
partial void HideHUDs(bool value)
{
if (isHUDsHidden == value) { return; }
if (value == true)
if (value)
{
GameMain.GameSession?.CrewManager?.AutoHideCrewList();
ToggleChatBox(false, storeOriginalState: true);
ChatBox.AutoHideChatBox();
}
else
{
GameMain.GameSession?.CrewManager?.ResetCrewList();
ToggleChatBox(chatBoxOriginalState, storeOriginalState: false);
GameMain.GameSession?.CrewManager?.ResetCrewListOpenState();
ChatBox.ResetChatBoxOpenState();
}
isHUDsHidden = value;
}
private void ToggleChatBox(bool value, bool storeOriginalState)
{
var crewManager = GameMain.GameSession?.CrewManager;
if (crewManager == null) { return; }
if (crewManager.IsSinglePlayer)
{
if (crewManager.ChatBox != null)
{
if (storeOriginalState)
{
chatBoxOriginalState = crewManager.ChatBox.ToggleOpen;
}
crewManager.ChatBox.ToggleOpen = value;
}
}
else if (GameMain.Client != null)
{
if (storeOriginalState)
{
chatBoxOriginalState = GameMain.Client.ChatBox.ToggleOpen;
}
GameMain.Client.ChatBox.ToggleOpen = value;
}
}
#if DEBUG
public override void CreateEditingHUD(SerializableEntityEditor editor)
{

View File

@@ -154,12 +154,14 @@ namespace Barotrauma.Items.Components
{
infoArea.Text = TextManager.Get(InfoText).Fallback(InfoText);
}
if (IsActive)
{
activateButton.Text = TextManager.Get("DeconstructorCancel");
infoArea.Text = string.Empty;
return;
}
bool outputsFound = false;
foreach (var (inputItem, deconstructItem) in GetAvailableOutputs(checkRequiredOtherItems: true))
{
@@ -174,27 +176,34 @@ namespace Barotrauma.Items.Components
}
inputItem.GetComponent<GeneticMaterial>()?.ModifyDeconstructInfo(this, ref buttonText, ref infoText);
activateButton.Text = buttonText;
if (infoArea != null)
{
infoArea.Text = infoText;
}
infoArea.Text = infoText;
return;
}
}
LocalizedString activateButtonText = TextManager.Get(ActivateButtonText);
activateButton.Enabled = outputsFound || !InputContainer.Inventory.IsEmpty();
activateButton.Text = activateButtonText;
//no valid outputs found: check if we're missing some required items from the input slots and display a message about it if possible
if (!outputsFound && infoArea != null)
{
foreach (var (inputItem, deconstructItem) in GetAvailableOutputs(checkRequiredOtherItems: false))
{
LocalizedString infoText = string.Empty;
if (deconstructItem.RequiredOtherItem.Any() && !string.IsNullOrEmpty(deconstructItem.InfoTextOnOtherItemMissing))
{
LocalizedString missingItemName = TextManager.Get("entityname." + deconstructItem.RequiredOtherItem.First());
infoArea.Text = TextManager.GetWithVariable(deconstructItem.InfoTextOnOtherItemMissing, "[itemname]", missingItemName);
infoText = TextManager.GetWithVariable(deconstructItem.InfoTextOnOtherItemMissing, "[itemname]", missingItemName);
}
inputItem.GetComponent<GeneticMaterial>()?.ModifyDeconstructInfo(this, ref activateButtonText, ref infoText);
activateButton.Text = activateButtonText;
infoArea.Text = infoText;
}
}
activateButton.Enabled = outputsFound || !InputContainer.Inventory.IsEmpty();
activateButton.Text = TextManager.Get(ActivateButtonText);
};
}
@@ -415,7 +424,7 @@ namespace Barotrauma.Items.Components
public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)
{
inSufficientPowerWarning.Visible = IsActive && !hasPower;
inSufficientPowerWarning.Visible = IsActive && !HasPower;
}
private bool OnActivateButtonClicked(GUIButton button, object obj)

View File

@@ -11,11 +11,21 @@ namespace Barotrauma.Items.Components
{
partial class Fabricator : Powered, IServerSerializable, IClientSerializable
{
private enum SortBy
{
Category,
Alphabetical,
SkillRequirement,
Price
}
private GUIListBox itemList;
private GUIFrame selectedItemFrame;
private GUIFrame selectedItemReqsFrame;
private GUILayoutGroup outputTopArea, paddedOutputArea;
private GUITextBlock amountTextMax;
private GUIScrollBar amountInput;
@@ -26,6 +36,8 @@ namespace Barotrauma.Items.Components
private GUIButton activateButton;
private GUITextBox itemFilterBox;
private GUITickBox availableOnlyTickBox;
private GUIDropDown sortByDropdown;
private GUIComponent outputSlot;
private GUIComponent inputInventoryHolder, outputInventoryHolder;
@@ -33,6 +45,9 @@ namespace Barotrauma.Items.Components
private readonly List<GUIButton> itemCategoryButtons = new List<GUIButton>();
private MapEntityCategory? selectedItemCategory;
private GUITextBlock requiresRecipeText;
private GUITextBlock nothingToShowText;
public FabricationRecipe SelectedItem
{
get { return selectedItem; }
@@ -65,6 +80,12 @@ namespace Barotrauma.Items.Components
[Serialize("vendingmachine.outofstock", IsPropertySaveable.Yes)]
public string FabricationLimitReachedText { get; set; }
[Serialize(true, IsPropertySaveable.No)]
public bool ShowSortByDropdown { get; set; }
[Serialize(true, IsPropertySaveable.No)]
public bool ShowAvailableOnlyTickBox { get; set; }
public override bool RecreateGUIOnResolutionChange => true;
protected override void OnResolutionChanged()
@@ -154,14 +175,13 @@ namespace Barotrauma.Items.Components
};
// === TOP AREA ===
var topFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.65f), mainFrame.RectTransform), style: "InnerFrameDark");
var topFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), mainFrame.RectTransform), style: "InnerFrameDark");
// === ITEM LIST ===
var itemListFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), topFrame.RectTransform), childAnchor: Anchor.Center);
var paddedItemFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), itemListFrame.RectTransform))
var paddedItemFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), itemListFrame.RectTransform), isHorizontal: false)
{
Stretch = true,
RelativeSpacing = 0.03f
Stretch = true
};
var filterArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedItemFrame.RectTransform), isHorizontal: true)
{
@@ -169,7 +189,8 @@ namespace Barotrauma.Items.Components
RelativeSpacing = 0.03f,
UserData = "filterarea"
};
new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.SubHeadingFont)
new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), filterArea.RectTransform), TextManager.Get("serverlog.filter"),
font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft)
{
Padding = Vector4.Zero,
AutoScaleVertical = true
@@ -183,29 +204,91 @@ namespace Barotrauma.Items.Components
FilterEntities(selectedItemCategory, text);
return true;
};
filterArea.RectTransform.MinSize = new Point(0, itemFilterBox.Rect.Height);
filterArea.RectTransform.MaxSize = new Point(int.MaxValue, itemFilterBox.Rect.Height);
itemList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), paddedItemFrame.RectTransform), style: null)
var sortByArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedItemFrame.RectTransform), isHorizontal: true)
{
Stretch = true,
RelativeSpacing = 0.03f,
Visible = ShowSortByDropdown,
IgnoreLayoutGroups = !ShowSortByDropdown
};
new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), sortByArea.RectTransform), TextManager.Get("campaignstore.sortby"),
font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft)
{
Padding = Vector4.Zero,
AutoScaleVertical = true
};
sortByDropdown = new GUIDropDown(new RectTransform(new Vector2(0.8f, 1.0f), sortByArea.RectTransform));
foreach (SortBy sortBy in Enum.GetValues<SortBy>())
{
sortByDropdown.AddItem(TextManager.Get("fabricator.sortby." + sortBy), userData: sortBy);
}
sortByDropdown.Select(index: 0);
sortByDropdown.AfterSelected += (GUIComponent selected, object userdata) =>
{
FilterEntities(selectedItemCategory, itemFilterBox.Text);
SortItems(character: Character.Controlled);
return true;
};
sortByArea.RectTransform.MinSize = new Point(0, sortByDropdown.Rect.Height);
sortByArea.RectTransform.MaxSize = new Point(int.MaxValue, sortByDropdown.Rect.Height);
var availableOnlyTickBoxArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedItemFrame.RectTransform), isHorizontal: true)
{
Stretch = true,
Visible = ShowAvailableOnlyTickBox,
IgnoreLayoutGroups = !ShowAvailableOnlyTickBox
};
new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), availableOnlyTickBoxArea.RectTransform), TextManager.Get("fabricator.onlyshowavailable"),
font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft)
{
Padding = Vector4.Zero,
AutoScaleVertical = true
};
availableOnlyTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f), availableOnlyTickBoxArea.RectTransform, scaleBasis: ScaleBasis.BothHeight), label: string.Empty)
{
ToolTip = TextManager.Get("fabricator.onlyshowavailable.tooltip")
};
availableOnlyTickBox.OnSelected += (tickbox) =>
{
FilterEntities(selectedItemCategory, itemFilterBox.Text);
return true;
};
availableOnlyTickBox.RectTransform.MinSize = new Point(availableOnlyTickBox.Rect.Height);
availableOnlyTickBox.RectTransform.IsFixedSize = true;
availableOnlyTickBoxArea.RectTransform.MinSize = new Point(0, availableOnlyTickBox.Rect.Height);
availableOnlyTickBoxArea.RectTransform.MaxSize = new Point(int.MaxValue, availableOnlyTickBox.Rect.Height);
itemList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), paddedItemFrame.RectTransform), style: null)
{
PlaySoundOnSelect = true,
OnSelected = (component, userdata) =>
{
selectedItem = userdata as FabricationRecipe;
if (selectedItem != null) { SelectItem(Character.Controlled, selectedItem); }
return true;
if (userdata is FabricationRecipe fabricationRecipe)
{
selectedItem = fabricationRecipe;
SelectItem(Character.Controlled, selectedItem);
return true;
}
else
{
return false;
}
}
};
// === SEPARATOR === //
new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), topFrame.RectTransform, Anchor.Center), style: "VerticalLine");
// === SEPARATOR === //
new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), topFrame.RectTransform, Anchor.Center), style: "VerticalLine");
// === OUTPUT AREA === //
var outputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), topFrame.RectTransform, Anchor.TopRight), childAnchor: Anchor.Center);
var paddedOutputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), outputArea.RectTransform));
var outputTopArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5F), paddedOutputArea.RectTransform, Anchor.Center), isHorizontal: true);
paddedOutputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), outputArea.RectTransform)) { Stretch = true };
outputTopArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), paddedOutputArea.RectTransform, Anchor.Center), isHorizontal: true);
// === OUTPUT SLOT === //
outputSlot = new GUIFrame(new RectTransform(new Vector2(0.4f, 1f), outputTopArea.RectTransform), style: null);
outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1.2f), outputSlot.RectTransform, Anchor.BottomCenter), style: null);
outputSlot = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.4f), outputTopArea.RectTransform, scaleBasis: ScaleBasis.BothWidth), style: null);
outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1.0f), outputSlot.RectTransform, Anchor.BottomCenter), style: null);
new GUICustomComponent(new RectTransform(Vector2.One, outputInventoryHolder.RectTransform), DrawOutputOverLay) { CanBeFocused = false };
// === DESCRIPTION === //
selectedItemFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 1f), outputTopArea.RectTransform), style: null);
@@ -213,7 +296,7 @@ namespace Barotrauma.Items.Components
selectedItemReqsFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), paddedOutputArea.RectTransform), style: null);
// === BOTTOM AREA === //
var bottomFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.3f), mainFrame.RectTransform), style: null);
var bottomFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.2f), mainFrame.RectTransform), style: null);
if (inputContainer.Capacity > 0)
{
@@ -298,6 +381,33 @@ namespace Barotrauma.Items.Components
CanBeFocused = false
};
CreateRecipes();
foreach (MapEntityCategory category in itemCategories)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform),
TextManager.Get("MapEntityCategory." + category), textColor: GUIStyle.TextColorBright)
{
CanBeFocused = false,
UserData = category,
Visible = false
};
}
requiresRecipeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform),
TextManager.Get("fabricatorrequiresrecipe"), textColor: Color.Red, font: GUIStyle.SubHeadingFont)
{
AutoScaleHorizontal = true,
CanBeFocused = false
};
nothingToShowText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.8f), itemList.Content.RectTransform), TextManager.Get("noitemsheader"),
textAlignment: Alignment.Center, textColor: GUIStyle.TextColorDim)
{
CanBeFocused = false,
Visible = false
};
SortItems(character: Character.Controlled);
}
private void RefreshActivateButtonText()
@@ -343,7 +453,8 @@ namespace Barotrauma.Items.Components
};
}
new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), container.RectTransform), GetRecipeNameAndAmount(fi))
new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), container.RectTransform),
RichString.Rich(GetRecipeNameAndAmount(fi)), font: GUIStyle.SmallFont)
{
Padding = Vector4.Zero,
AutoScaleVertical = true,
@@ -372,17 +483,17 @@ namespace Barotrauma.Items.Components
outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform;
}
private static LocalizedString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe)
private static RichString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe)
{
if (fabricationRecipe == null) { return ""; }
if (fabricationRecipe.Amount > 1)
{
return TextManager.GetWithVariables("fabricationrecipenamewithamount",
("[name]", fabricationRecipe.DisplayName), ("[amount]", fabricationRecipe.Amount.ToString()));
("[name]", RichString.Rich(fabricationRecipe.DisplayName)), ("[amount]", fabricationRecipe.Amount.ToString()));
}
else
{
return fabricationRecipe.DisplayName;
return RichString.Rich(fabricationRecipe.DisplayName);
}
}
@@ -397,73 +508,106 @@ namespace Barotrauma.Items.Components
if (character != Character.Controlled) { return; }
var nonItems = itemList.Content.Children.Where(c => c.UserData is not FabricationRecipe).ToList();
nonItems.ForEach(i => itemList.Content.RemoveChild(i));
nonItems.ForEach(i => i.Visible = false);
SortItems(character: null);
FilterEntities(selectedItemCategory, itemFilterBox?.Text ?? string.Empty);
HideEmptyItemListCategories();
}
private void SortItems(Character character)
{
SortBy sortBy = (SortBy)sortByDropdown.SelectedData;
itemList.Content.RectTransform.SortChildren((c1, c2) =>
{
var item1 = c1.GUIComponent.UserData as FabricationRecipe;
var item2 = c2.GUIComponent.UserData as FabricationRecipe;
int itemPlacement1 = calculatePlacement(item1);
int itemPlacement2 = calculatePlacement(item2);
if (itemPlacement1 != itemPlacement2)
if (item1 == null && item2 == null)
{
return itemPlacement1 > itemPlacement2 ? -1 : 1;
return 0;
}
else if (item1 == null)
{
return -1;
}
else if (item2 == null)
{
return 1;
}
int calculatePlacement(FabricationRecipe recipe)
bool missingRecipe1 = MissingRequiredRecipe(item1, character);
bool missingRecipe2 = MissingRequiredRecipe(item2, character);
if (missingRecipe1 != missingRecipe2)
{
if (recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem))
{
return -2;
}
int placement = FabricationDegreeOfSuccess(character, recipe.RequiredSkills) >= 0.5f ? 0 : -1;
return placement;
return missingRecipe1.CompareTo(missingRecipe2);
}
return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value);
switch (sortBy)
{
case SortBy.Alphabetical:
return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value);
case SortBy.Category:
var category1 = EnumExtensions.GetIndividualFlags(item1.TargetItem.Category).FirstOrDefault();
var category2 = EnumExtensions.GetIndividualFlags(item2.TargetItem.Category).FirstOrDefault();
if (category1 == category2)
{
return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value);
}
return category1.CompareTo(category2);
case SortBy.SkillRequirement:
float skillRequirement1 = item1.RequiredSkills.Sum(skill => skill.Level);
float skillRequirement2 = item2.RequiredSkills.Sum(skill => skill.Level);
if (MathUtils.NearlyEqual(skillRequirement1, skillRequirement2))
{
return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value);
}
return skillRequirement1.CompareTo(skillRequirement2);
case SortBy.Price:
float itemValue1 = item1.TargetItem.DefaultPrice?.Price ?? 0;
float itemValue2 = item2.TargetItem.DefaultPrice?.Price ?? 0;
if (MathUtils.NearlyEqual(itemValue1, itemValue2))
{
return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value);
}
return itemValue2.CompareTo(itemValue1);
default:
throw new NotImplementedException($"Sorting by {sortBy} has not been implemented.");
}
});
var sufficientSkillsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform),
TextManager.Get("fabricatorsufficientskills"), textColor: GUIStyle.Green, font: GUIStyle.SubHeadingFont)
if (sortBy == SortBy.Category)
{
AutoScaleHorizontal = true,
CanBeFocused = false
};
sufficientSkillsText.RectTransform.SetAsFirstChild();
foreach (var categoryText in itemList.Content.Children.Where(c => c.UserData?.GetType() == typeof(MapEntityCategory)).ToList())
{
categoryText.RectTransform.SetAsLastChild();
var category = (MapEntityCategory)categoryText.UserData;
var firstChildWithMatchingCategory = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe recipe && EnumExtensions.GetIndividualFlags(recipe.TargetItem.Category).FirstOrDefault() == category);
if (firstChildWithMatchingCategory != null)
{
categoryText.RectTransform.RepositionChildInHierarchy(itemList.Content.GetChildIndex(firstChildWithMatchingCategory));
categoryText.Visible = true;
}
else
{
categoryText.Visible = false;
}
}
}
var insufficientSkillsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform),
TextManager.Get("fabricatorinsufficientskills"), textColor: Color.Orange, font: GUIStyle.SubHeadingFont)
requiresRecipeText.RectTransform.SetAsLastChild();
var firstMissingRecipe = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe recipe && MissingRequiredRecipe(recipe, character));
if (firstMissingRecipe != null)
{
AutoScaleHorizontal = true,
CanBeFocused = false
};
var firstinSufficient = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe fabricableItem && FabricationDegreeOfSuccess(character, fabricableItem.RequiredSkills) < 0.5f);
if (firstinSufficient != null)
{
insufficientSkillsText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstinSufficient.RectTransform));
requiresRecipeText.RectTransform.RepositionChildInHierarchy(itemList.Content.GetChildIndex(firstMissingRecipe));
requiresRecipeText.Visible = true;
}
else
{
sufficientSkillsText.Visible = insufficientSkillsText.Visible = false;
sufficientSkillsText.Enabled = insufficientSkillsText.Enabled = false;
requiresRecipeText.Visible = false;
}
var requiresRecipeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform),
TextManager.Get("fabricatorrequiresrecipe"), textColor: Color.Red, font: GUIStyle.SubHeadingFont)
{
AutoScaleHorizontal = true,
CanBeFocused = false
};
var firstRequiresRecipe = itemList.Content.Children.FirstOrDefault(c =>
c.UserData is FabricationRecipe fabricableItem &&
fabricableItem.RequiresRecipe && !AnyOneHasRecipeForItem(character, fabricableItem.TargetItem));
if (firstRequiresRecipe != null)
{
requiresRecipeText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstRequiresRecipe.RectTransform));
}
FilterEntities(selectedItemCategory, itemFilterBox?.Text ?? string.Empty);
HideEmptyItemListCategories();
}
@@ -757,6 +901,9 @@ namespace Barotrauma.Items.Components
private bool FilterEntities(MapEntityCategory? category, string filter)
{
bool onlyShowAvailable = availableOnlyTickBox is { Selected: true };
bool anyVisible = false;
foreach (GUIComponent child in itemList.Content.Children)
{
FabricationRecipe recipe = child.UserData as FabricationRecipe;
@@ -771,9 +918,26 @@ namespace Barotrauma.Items.Components
}
}
if (recipe.RequiresRecipe && recipe.HideIfNoRecipe)
{
if (Character.Controlled != null)
{
if (!AnyOneHasRecipeForItem(Character.Controlled, recipe.TargetItem))
{
child.Visible = false;
continue;
}
}
}
child.Visible =
(string.IsNullOrWhiteSpace(filter) || recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)) &&
(!category.HasValue || recipe.TargetItem.Category.HasFlag(category.Value));
(!category.HasValue || recipe.TargetItem.Category.HasFlag(category.Value)) &&
(!onlyShowAvailable || CanBeFabricated(recipe, availableIngredients, Character.Controlled));
if (child.Visible)
{
anyVisible = true;
}
}
foreach (GUIButton btn in itemCategoryButtons)
@@ -781,6 +945,8 @@ namespace Barotrauma.Items.Components
btn.Selected = (MapEntityCategory?)btn.UserData == selectedItemCategory;
}
HideEmptyItemListCategories();
nothingToShowText.Visible = !anyVisible;
itemList.UserData = "itemlist";
return true;
}
@@ -788,7 +954,7 @@ namespace Barotrauma.Items.Components
private void HideEmptyItemListCategories()
{
bool visibleElementsChanged = false;
//go through the elements backwards, and disable the labels ("insufficient skills to fabricate", "recipe required...") if there's no items below them
//go through the elements backwards, and disable the labels if there's no items below them
bool recipeVisible = false;
foreach (GUIComponent child in itemList.Content.Children.Reverse())
{
@@ -810,6 +976,12 @@ namespace Barotrauma.Items.Components
}
}
SortBy sortBy = (SortBy)sortByDropdown.SelectedData;
if (sortBy != SortBy.Category)
{
itemList.Content.Children.Where(c => c.UserData?.GetType() == typeof(MapEntityCategory)).ForEach(c => c.Visible = false);
}
if (visibleElementsChanged)
{
itemList.UpdateScrollBarSize();
@@ -841,8 +1013,8 @@ namespace Barotrauma.Items.Components
private void CreateSelectedItemUI(SelectedRecipe recipe)
{
var (user, selectedItem, overrideRequiredTime) = recipe;
int max = Math.Max(selectedItem.TargetItem.GetMaxStackSize(outputContainer.Inventory) / selectedItem.Amount, 1);
var (user, selectedRecipe, overrideRequiredTime) = recipe;
int max = Math.Max(selectedRecipe.TargetItem.GetMaxStackSize(outputContainer.Inventory) / selectedRecipe.Amount, 1);
if (amountInput != null)
{
@@ -859,18 +1031,59 @@ namespace Barotrauma.Items.Components
selectedItemFrame.ClearChildren();
selectedItemReqsFrame.ClearChildren();
var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f };
var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f, CanBeFocused = true };
var paddedReqFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemReqsFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f };
LocalizedString itemName = GetRecipeNameAndAmount(selectedItem);
LocalizedString itemName = GetRecipeNameAndAmount(selectedRecipe);
LocalizedString name = itemName;
QualityResult result = GetFabricatedItemQuality(selectedItem, user);
QualityResult result = GetFabricatedItemQuality(selectedRecipe, user);
float quality = selectedItem.Quality ?? result.Quality;
if (quality > 0 || result.HasRandomQualityRollChance)
float minimumQuality = selectedRecipe.Quality ?? result.Quality;
LocalizedString qualityTooltip = string.Empty;
if (result.HasRandomQualityRollChance)
{
name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName + '\n')
float plusOnePercentage = result.TotalPlusOnePercentage;
float plusTwoPercentage = result.TotalPlusTwoPercentage;
string plusOnePercentageText = plusOnePercentage.ToString("F1", CultureInfo.InvariantCulture);
string plusTwoPercentageText = plusTwoPercentage.ToString("F1", CultureInfo.InvariantCulture);
int plusOneQuality = Math.Clamp(result.Quality + 1, min: 0, max: 3);
int plusTwoQuality = Math.Clamp(result.Quality + 2, min: 0, max: 3);
LocalizedString plusOneQualityText = TextManager.Get($"quality{plusOneQuality}");
LocalizedString plusTwoQualityText = TextManager.Get($"quality{plusTwoQuality}");
string localizationTag = plusTwoPercentage > 0f && plusOnePercentage > 0 && plusOneQuality != plusTwoQuality ? "meetsbonusrequirementtwice" : "meetsbonusrequirement";
var variables = new (string Key, LocalizedString Value)[]
{
("[chance]", plusOnePercentageText), ("[quality]", plusOneQualityText),
("[chance2]", plusTwoPercentageText), ("[quality2]", plusTwoQualityText)
};
if (MathUtils.NearlyEqual(plusOnePercentage, 0))
{
variables = new[] { ("[chance]", plusTwoPercentageText), ("[quality]", plusTwoQualityText) };
}
if (plusOneQuality == plusTwoQuality)
{
LocalizedString rawPercentage = result.PlusOnePercentage.ToString("F1", CultureInfo.InvariantCulture);
variables = new[] { ("[chance]", rawPercentage), ("[quality]", plusOneQualityText) };
}
if (plusOnePercentage >= 100.0f) { minimumQuality = plusOneQuality; }
if (plusTwoPercentage >= 100.0f) { minimumQuality = plusTwoQuality; }
qualityTooltip = TextManager.GetWithVariables(localizationTag, variables);
}
if (minimumQuality > 0 || result.HasRandomQualityRollChance)
{
name = TextManager.GetWithVariable("itemname.quality" + (int)minimumQuality, "[itemname]", itemName + '\n')
.Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName + '\n'));
}
@@ -884,44 +1097,13 @@ namespace Barotrauma.Items.Components
{
var iconLayout = new GUIFrame(new RectTransform(new Vector2(0.4f, 1f), selectedItemFrame.RectTransform, anchor: Anchor.TopRight), style: null);
var icon = GameSession.CreateNotificationIcon(iconLayout, offset: true);
float percentage1 = result.TotalPlusOnePercentage;
float percentage2 = result.TotalPlusTwoPercentage;
string chance1text = percentage1.ToString("F1", CultureInfo.InvariantCulture);
string chance2text = percentage2.ToString("F1", CultureInfo.InvariantCulture);
int quality1 = Math.Clamp(result.Quality + 1, min: 0, max: 3);
int quality2 = Math.Clamp(result.Quality + 2, min: 0, max: 3);
LocalizedString quality1Text = TextManager.Get($"quality{quality1}");
LocalizedString quality2Text = TextManager.Get($"quality{quality2}");
string localizationTag = percentage2 > 0f && percentage1 > 0 && quality1 != quality2 ? "meetsbonusrequirementtwice" : "meetsbonusrequirement";
var variables = new (string Key, LocalizedString Value)[]
{
("[chance]", chance1text), ("[quality]", quality1Text),
("[chance2]", chance2text), ("[quality2]", quality2Text)
};
if (MathUtils.NearlyEqual(percentage1, 0))
{
variables = new[] { ("[chance]", chance2text), ("[quality]", quality2Text) };
}
if (quality1 == quality2)
{
LocalizedString rawPercentage = result.PlusOnePercentage.ToString("F1", CultureInfo.InvariantCulture);
variables = new[] { ("[chance]", rawPercentage), ("[quality]", quality1Text) };
}
LocalizedString qualityTooltip = TextManager.GetWithVariables(localizationTag, variables);
icon.ToolTip = RichString.Rich(qualityTooltip);
icon.Visible = icon.CanBeFocused = true;
}
outputTopArea.RectTransform.MaxSize = new Point(int.MaxValue, outputInventoryHolder.Rect.Height);
paddedOutputArea.Recalculate();
nameBlock.Padding = new Vector4(0, nameBlock.Padding.Y, GUI.IntScale(5), nameBlock.Padding.W);
if (nameBlock.TextScale < 0.7f)
{
@@ -932,31 +1114,41 @@ namespace Barotrauma.Items.Components
nameBlock.RectTransform.MinSize = new Point(0, (int)(nameBlock.TextSize.Y * nameBlock.TextScale));
}
if (!selectedItem.TargetItem.Description.IsNullOrEmpty())
bool largeUI = GuiFrame.Rect.Height > GUI.IntScale(500);
if (largeUI)
{
var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform),
RichString.Rich(selectedItem.TargetItem.Description),
font: GUIStyle.SmallFont, wrap: true);
description.Padding = new Vector4(0, description.Padding.Y, description.Padding.Z, description.Padding.W);
paddedFrame.ChildAnchor = Anchor.CenterLeft;
}
while (description.Rect.Height + nameBlock.Rect.Height > paddedFrame.Rect.Height)
if (!selectedRecipe.TargetItem.Description.IsNullOrEmpty())
{
var descriptionParent = largeUI ? paddedReqFrame : paddedFrame;
var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), descriptionParent.RectTransform),
RichString.Rich(selectedRecipe.TargetItem.Description),
font: GUIStyle.SmallFont, wrap: true);
if (!largeUI)
{
description.Padding = new Vector4(0, description.Padding.Y, description.Padding.Z, description.Padding.W);
}
while (description.Rect.Height + nameBlock.Rect.Height > descriptionParent.Rect.Height)
{
var lines = description.WrappedText.Split('\n');
if (lines.Count <= 1) { break; }
var newString = string.Join('\n', lines.Take(lines.Count - 1));
description.Text = newString.Substring(0, newString.Length - 4) + "...";
description.CalculateHeightFromText();
description.ToolTip = selectedItem.TargetItem.Description;
description.ToolTip = selectedRecipe.TargetItem.Description;
}
}
IEnumerable<Skill> inadequateSkills = Enumerable.Empty<Skill>();
if (user != null)
{
inadequateSkills = selectedItem.RequiredSkills.Where(skill => user.GetSkillLevel(skill.Identifier) < Math.Round(skill.Level * SkillRequirementMultiplier));
inadequateSkills = selectedRecipe.RequiredSkills.Where(skill => user.GetSkillLevel(skill.Identifier) < Math.Round(skill.Level * SkillRequirementMultiplier));
}
if (selectedItem.RequiredSkills.Any())
if (selectedRecipe.RequiredSkills.Any())
{
LocalizedString text = "";
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform),
@@ -965,20 +1157,20 @@ namespace Barotrauma.Items.Components
AutoScaleHorizontal = true,
ToolTip = TextManager.Get("fabricatorrequiredskills.tooltip")
};
foreach (Skill skill in selectedItem.RequiredSkills)
foreach (Skill skill in selectedRecipe.RequiredSkills)
{
text += TextManager.Get("SkillName." + skill.Identifier) + " " + TextManager.Get("Lvl").ToLower() + " " + Math.Round(skill.Level * SkillRequirementMultiplier);
if (skill != selectedItem.RequiredSkills.Last()) { text += "\n"; }
if (skill != selectedRecipe.RequiredSkills.Last()) { text += "\n"; }
}
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), text, font: GUIStyle.SmallFont);
}
float degreeOfSuccess = user == null ? 0.0f : FabricationDegreeOfSuccess(user, selectedItem.RequiredSkills);
float degreeOfSuccess = user == null ? 0.0f : FabricationDegreeOfSuccess(user, selectedRecipe.RequiredSkills);
if (degreeOfSuccess > 0.5f) { degreeOfSuccess = 1.0f; }
float requiredTime = overrideRequiredTime.TryUnwrap(out var time)
? time
: (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user));
: (user == null ? selectedRecipe.RequiredTime : GetRequiredTime(selectedRecipe, user));
if ((int)requiredTime > 0)
{
@@ -991,7 +1183,7 @@ namespace Barotrauma.Items.Components
font: GUIStyle.SmallFont);
}
if (SelectedItem.RequiredMoney > 0)
if (selectedRecipe.RequiredMoney > 0)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform),
TextManager.Get("subeditor.price"), textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont)
@@ -1000,7 +1192,6 @@ namespace Barotrauma.Items.Components
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), TextManager.FormatCurrency(SelectedItem.RequiredMoney),
font: GUIStyle.SmallFont);
}
}
@@ -1031,7 +1222,7 @@ namespace Barotrauma.Items.Components
{
if (selectedItem == null) { return false; }
if (fabricatedItem == null &&
!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health))
!outputContainer.Inventory.CanProbablyBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health))
{
outputSlot.Flash(GUIStyle.Red);
return false;
@@ -1101,8 +1292,14 @@ namespace Barotrauma.Items.Components
activateButton.Enabled = canBeFabricated;
}
bool sufficientSkills = FabricationDegreeOfSuccess(character, recipe.RequiredSkills) >= 0.5f;
Color baseColor = MissingRequiredRecipe(recipe, character) ?
GUIStyle.Red :
(sufficientSkills ? GUIStyle.TextColorNormal : GUIStyle.Orange);
var childContainer = child.GetChild<GUILayoutGroup>();
childContainer.GetChild<GUITextBlock>().TextColor = Color.White * (canBeFabricated ? 1.0f : 0.5f);
childContainer.GetChild<GUITextBlock>().TextColor = baseColor * (canBeFabricated ? 1.0f : 0.5f);
childContainer.GetChild<GUIImage>().Color = recipe.TargetItem.InventoryIconColor * (canBeFabricated ? 1.0f : 0.5f);
var limitReachedText = child.FindChild(nameof(FabricationLimitReachedText));

View File

@@ -10,21 +10,10 @@ using Microsoft.Xna.Framework.Input;
namespace Barotrauma.Items.Components
{
internal readonly struct MiniMapGUIComponent
internal readonly record struct MiniMapGUIComponent(GUIComponent RectComponent, GUIComponent BorderComponent)
{
public readonly GUIComponent RectComponent;
public readonly GUIComponent BorderComponent;
public MiniMapGUIComponent(GUIComponent rectComponent)
public MiniMapGUIComponent(GUIComponent rectComponent) : this(rectComponent, rectComponent)
{
RectComponent = rectComponent;
BorderComponent = rectComponent;
}
public MiniMapGUIComponent(GUIComponent frame, GUIComponent linkedHullComponent)
{
RectComponent = frame;
BorderComponent = linkedHullComponent;
}
public void Deconstruct(out GUIComponent component, out GUIComponent borderComponent)
@@ -34,21 +23,14 @@ namespace Barotrauma.Items.Components
}
}
internal readonly struct MiniMapSprite
internal readonly record struct MiniMapSprite(Sprite? Sprite, Color Color)
{
public readonly Sprite? Sprite;
public readonly Color Color;
public MiniMapSprite(JobPrefab prefab)
public MiniMapSprite(JobPrefab prefab) : this(prefab.IconSmall, prefab.UIColor)
{
Sprite = prefab.IconSmall;
Color = prefab.UIColor;
}
public MiniMapSprite(Order order)
public MiniMapSprite(Order order) : this(order.SymbolSprite, order.Color)
{
Sprite = order.SymbolSprite;
Color = order.Color;
}
}
@@ -224,6 +206,26 @@ namespace Barotrauma.Items.Components
ElectricalBaseColor = GUIStyle.Orange,
NoPowerElectricalColor = ElectricalBaseColor * 0.1f;
// If this is portable, only allow displaying data in the player sub (not enemy subs, ruins, wrecks or other unknown places)
private bool IsPortableItemAllowed
{
get
{
if (IsUsableOutsidePlayerSub) { return true; }
if (item.Submarine == null) { return false; }
if (item.GetComponent<Pickable>() is not Pickable handheldItem) { return true; }
// This will effectively make sure wherever we are, it belongs to the player
return handheldItem.Picker?.TeamID == item.Submarine.TeamID;
}
}
[Serialize(false, IsPropertySaveable.No, description: "If this item is portable, should it be usable outside the player submarine?")]
public bool IsUsableOutsidePlayerSub
{
get;
set;
}
partial void InitProjSpecific()
{
hullDatas = new Dictionary<Hull, HullData>();
@@ -425,22 +427,25 @@ namespace Barotrauma.Items.Components
return false;
}
private bool VisibleOnItemFinder(Item it)
private bool VisibleOnItemFinder(Item targetItem)
{
if (it?.Submarine == null) { return false; }
if (item.Submarine == null || !item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; }
if (it.NonInteractable || it.IsHidden) { return false; }
if (it.GetComponent<Pickable>() == null) { return false; }
if (targetItem?.Submarine == null || item.Submarine == null) { return false; }
var holdable = it.GetComponent<Holdable>();
if (!IsPortableItemAllowed) { return false; }
if (!item.Submarine.IsEntityFoundOnThisSub(targetItem, includingConnectedSubs: true)) { return false; }
if (targetItem.NonInteractable || targetItem.IsHidden) { return false; }
if (targetItem.GetComponent<Pickable>() == null) { return false; }
var holdable = targetItem.GetComponent<Holdable>();
if (holdable != null && holdable.Attached) { return false; }
var wire = it.GetComponent<Wire>();
var wire = targetItem.GetComponent<Wire>();
if (wire != null && wire.Connections.Any(c => c != null)) { return false; }
if (it.Container?.GetComponent<ItemContainer>() is { DrawInventory: false } or { AllowAccess: false }) { return false; }
if (targetItem.Container?.GetComponent<ItemContainer>() is { DrawInventory: false } or { AllowAccess: false }) { return false; }
if (it.HasTag(Tags.TraitorMissionItem)) { return false; }
if (targetItem.HasTag(Tags.TraitorMissionItem)) { return false; }
return true;
}
@@ -455,18 +460,24 @@ namespace Barotrauma.Items.Components
}
}
private void CreateHUD()
private void ClearHUD()
{
subEntities.Clear();
prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
submarineContainer.ClearChildren();
displayedSubs.Clear();
}
if (item.Submarine is null)
private void RefreshHUD()
{
ClearHUD();
if (item.Submarine is null || !IsPortableItemAllowed)
{
displayedSubs.Clear();
return;
}
prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center));
miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false };
@@ -574,18 +585,25 @@ namespace Barotrauma.Items.Components
public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)
{
//recreate HUD if the subs we should display have changed
if (item.Submarine == null && displayedSubs.Count > 0 || // item not inside a sub anymore, but display is still showing subs
// Refresh HUD (including possibly just clearing it away) if the subs we should display have changed
if (item.Submarine == null && displayedSubs.Count > 0 || // item not inside a sub anymore, but display is still showing subs
item.Submarine is { } itemSub &&
(
!displayedSubs.Contains(itemSub) || // current sub not displayed
itemSub.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID).Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || // some of the docked subs not displayed
displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed
// current sub not displayed
!displayedSubs.Contains(itemSub) ||
// some of the docked subs not displayed
itemSub.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID).Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) ||
// displaying a sub that shouldn't be displayed
displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s))
) ||
prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || // resolution changed
!submarineContainer.Children.Any()) // We lack a GUI
// If this item is portable and not in a player sub and using it otherwise is disallowed
!IsPortableItemAllowed ||
// resolution changed
prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight ||
// We lack a GUI
!submarineContainer.Children.Any())
{
CreateHUD();
RefreshHUD();
}
//reset data if we haven't received anything in a while
@@ -737,7 +755,7 @@ namespace Barotrauma.Items.Components
return;
}
if (Voltage < MinVoltage)
if (!HasPower)
{
Vector2 textSize = GUIStyle.Font.MeasureString(noPowerTip);
Vector2 textPos = GuiFrame.Rect.Center.ToVector2();
@@ -747,7 +765,7 @@ namespace Barotrauma.Items.Components
return;
}
if (currentMode == MiniMapMode.HullStatus && item.Submarine != null)
if (currentMode == MiniMapMode.HullStatus && item.Submarine != null && IsPortableItemAllowed)
{
Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
spriteBatch.End();
@@ -958,19 +976,19 @@ namespace Barotrauma.Items.Components
MiniMapBlips = positions.ToImmutableHashSet();
if (searchAutoComplete is null) { return; }
searchAutoComplete.Visible = false;
HideGUIComponent(searchAutoComplete);
}
private void UpdateHUDBack()
{
if (item.Submarine == null) { return; }
// Clear up mode-specific elements before checking if drawing should continue, so they'll be gone if not
HideModeSpecificFrames();
if (hullInfoFrame != null) { hullInfoFrame.Visible = false; }
reportFrame.Visible = false;
searchBarFrame.Visible = false;
electricalFrame.Visible = false;
miniMapFrame.Visible = false;
if (item.Submarine == null || !IsPortableItemAllowed)
{
ClearHUD();
return;
}
switch (currentMode)
{
@@ -989,6 +1007,23 @@ namespace Barotrauma.Items.Components
}
}
private void HideModeSpecificFrames()
{
HideGUIComponent(hullInfoFrame);
HideGUIComponent(reportFrame);
HideGUIComponent(searchBarFrame);
HideGUIComponent(electricalFrame);
HideGUIComponent(miniMapFrame);
}
private static void HideGUIComponent(GUIComponent? component)
{
if (component != null)
{
component.Visible = false;
}
}
private void UpdateHullStatus()
{
bool canHoverOverHull = true;
@@ -1007,7 +1042,7 @@ namespace Barotrauma.Items.Components
child.Color = child.OutlineColor = NoPowerDoorColor;
}
if (Voltage < MinVoltage) { continue; }
if (!HasPower) { continue; }
child.Color = child.OutlineColor = DoorIndicatorColor;
if (GUI.MouseOn == child)
@@ -1037,7 +1072,7 @@ namespace Barotrauma.Items.Components
}
}
if (Voltage < MinVoltage) { continue; }
if (!HasPower) { continue; }
hullDatas.TryGetValue(hull, out HullData? hullData);
if (hullData is null)
@@ -1187,7 +1222,7 @@ namespace Barotrauma.Items.Components
component.Color = component.OutlineColor = NoPowerElectricalColor;
}
if (Voltage < MinVoltage || !miniMapGuiComponent.RectComponent.Visible) { continue; }
if (!HasPower || !miniMapGuiComponent.RectComponent.Visible) { continue; }
int durability = (int)(it.Condition / (it.MaxCondition / it.MaxRepairConditionMultiplier) * 100f);
Color color = ToolBox.GradientLerp(durability / 100f, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green, GUIStyle.Green);
@@ -1229,11 +1264,11 @@ namespace Barotrauma.Items.Components
private void DrawHUDBack(SpriteBatch spriteBatch, GUICustomComponent container)
{
if (item.Submarine == null) { return; }
if (item.Submarine == null || !IsPortableItemAllowed) { return; }
DrawSubmarine(spriteBatch);
if (Voltage < MinVoltage) { return; }
if (!HasPower) { return; }
Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);

View File

@@ -475,11 +475,8 @@ namespace Barotrauma.Items.Components
if (sound != null)
{
SoundPlayer.PlaySound(
sound.Sound,
sound,
item.WorldPosition,
sound.Volume,
sound.Range,
freqMult: sound.GetRandomFrequencyMultiplier(),
hullGuess: item.CurrentHull);
}
}

View File

@@ -144,6 +144,7 @@ namespace Barotrauma.Items.Components
private bool isConnectedToSteering;
private static LocalizedString caveLabel;
private static LocalizedString enemyLabel;
[Serialize(false, IsPropertySaveable.Yes)]
@@ -164,6 +165,8 @@ namespace Barotrauma.Items.Components
TextManager.Get("cave").Fallback(
TextManager.Get("missiontype.nest"));
enemyLabel = TextManager.Get("enemysubmarine");
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -216,8 +219,9 @@ namespace Barotrauma.Items.Components
Vector2 size = isConnectedToSteering ? controlBoxSize : new Vector2(0.46f, 0.4f);
controlContainer = new GUIFrame(new RectTransform(size, GuiFrame.RectTransform, Anchor.BottomLeft), "ItemUI");
if (!isConnectedToSteering && !GUI.IsFourByThree())
if (!isConnectedToSteering && GUI.AspectRatioDifference <= 0)
{
// In wider than 4:3 aspect ratio, we'll limit the max size.
controlContainer.RectTransform.MaxSize = new Point((int)(380 * GUI.xScale), (int)(300 * GUI.yScale));
}
var paddedControlContainer = new GUIFrame(new RectTransform(controlContainer.Rect.Size - GUIStyle.ItemFrameMargin, controlContainer.RectTransform, Anchor.Center)
@@ -1019,6 +1023,38 @@ namespace Barotrauma.Items.Components
cave.StartPos.ToVector2(), transducerCenter,
DisplayScale, center, DisplayRadius);
}
if (GameMain.NetworkMember is { } networkMember && GameMain.GameSession?.GameMode is PvPMode)
{
if (networkMember.ServerSettings.TrackOpponentInPvP
&& Submarine.MainSubs[0] is { } coalitionSub
&& Submarine.MainSubs[1] is { } separatistSub
&& Character.Controlled is { } player)
{
Submarine whichSubToDraw = player.TeamID switch
{
CharacterTeamType.Team1 => separatistSub,
CharacterTeamType.Team2 => coalitionSub,
_ => null
};
if (whichSubToDraw != null)
{
DrawOffsetMarker(spriteBatch,
enemyLabel.Value,
Tags.Submarine,
Tags.Enemy,
whichSubToDraw.WorldPosition,
transducerCenter,
distanceThresholds: new Range<float>(start: MetersToUnits(150), end: MetersToUnits(1600)),
offset: new Range<float>(start: MetersToUnits(100), end: MetersToUnits(400)),
minOffset: MetersToUnits(10));
static float MetersToUnits(float m)
=> m / Physics.DisplayToRealWorldRatio;
}
}
}
}
int missionIndex = 0;
@@ -1042,7 +1078,8 @@ namespace Barotrauma.Items.Components
}
if (HasMineralScanner && UseMineralScanner && CurrentMode == Mode.Active && MineralClusters != null &&
(item.CurrentHull == null || !DetectSubmarineWalls))
(item.CurrentHull == null || !DetectSubmarineWalls) &&
HasPower)
{
foreach (var c in MineralClusters)
{
@@ -1076,7 +1113,7 @@ namespace Barotrauma.Items.Components
{
if (!sub.ShowSonarMarker) { continue; }
if (connectedSubs.Contains(sub)) { continue; }
if (Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; }
if (sub.IsAboveLevel) { continue; }
if (item.Submarine != null || Character.Controlled != null)
{
@@ -1185,7 +1222,7 @@ namespace Barotrauma.Items.Components
foreach (DockingPort dockingPort in DockingPort.List)
{
if (Level.Loaded != null && dockingPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; }
if (dockingPort.Item.Submarine.IsAboveLevel) { continue; }
if (dockingPort.Item.IsHidden) { continue; }
if (dockingPort.Item.Submarine == null) { continue; }
if (dockingPort.Item.Submarine.Info.IsWreck) { continue; }
@@ -1198,8 +1235,8 @@ namespace Barotrauma.Items.Components
//don't show the docking ports of the opposing team on the sonar
if (item.Submarine != null &&
item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle &&
dockingPort.Item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle &&
!item.Submarine.IsRespawnShuttle &&
!dockingPort.Item.Submarine.IsRespawnShuttle &&
!dockingPort.Item.Submarine.Info.IsOutpost &&
!dockingPort.Item.Submarine.Info.IsBeacon)
{
@@ -1792,8 +1829,47 @@ namespace Barotrauma.Items.Components
sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f * blip.Alpha, sonarBlip.Origin, 0, scale, SpriteEffects.None, 0);
}
/// <summary>
/// Used in DrawOffsetMarker to cache the randomized location of the marker
/// </summary>
private readonly Dictionary<Identifier, CachedLocation> cachedLocations = new Dictionary<Identifier, CachedLocation>();
private void DrawOffsetMarker(SpriteBatch spriteBatch, string label, Identifier iconIdentifier, Identifier targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, Range<float> distanceThresholds, Range<float> offset, float minOffset)
{
Vector2 pos;
if (!cachedLocations.TryGetValue(targetIdentifier, out CachedLocation cachedLocation))
{
cachedLocation = CreateCachedLocation();
cachedLocations.Add(targetIdentifier, cachedLocation);
pos = cachedLocation.Location;
}
else
{
if (Timing.TotalTime > cachedLocation.RecalculationTime)
{
cachedLocation = CreateCachedLocation();
cachedLocations[targetIdentifier] = cachedLocation;
}
pos = cachedLocation.Location;
}
DrawMarker(spriteBatch, label, iconIdentifier, targetIdentifier, pos, transducerPosition, DisplayScale, center, DisplayRadius);
CachedLocation CreateCachedLocation()
{
float distance = Vector2.Distance(worldPosition, transducerPosition);
float maxOffset = MathHelper.Lerp(offset.Start, offset.End, MathHelper.Clamp((distance - distanceThresholds.Start) / (distanceThresholds.End - distanceThresholds.Start), 0.0f, 1.0f));
Vector2 randomPos = Rand.Vector(Rand.Range(minOffset, maxOffset));
return new CachedLocation(worldPosition + randomPos, Timing.TotalTime + Rand.Range(10.0f, 30.0f));
}
}
private void DrawMarker(SpriteBatch spriteBatch, string label, Identifier iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius,
bool onlyShowTextOnMouseOver = false)
bool onlyShowTextOnMouseOver = false)
{
float linearDist = Vector2.Distance(worldPosition, transducerPosition);
float dist = linearDist;
@@ -1903,25 +1979,6 @@ namespace Barotrauma.Items.Components
2, GUIStyle.SmallFont);
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
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;
}
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
{
msg.WriteBoolean(currentMode == Mode.Active);

View File

@@ -57,24 +57,42 @@ namespace Barotrauma.Items.Components
private GUIMessageBox enterOutpostPrompt, exitOutpostPrompt;
private bool levelStartSelected;
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)]
public bool LevelStartSelected
{
get { return levelStartTickBox.Selected; }
set { levelStartTickBox.Selected = value; }
get
{
return levelStartTickBox?.Selected ?? levelStartSelected;
}
set
{
TrySetTickBoxSelected(levelStartTickBox, ref levelStartSelected, value);
}
}
private bool levelEndSelected;
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)]
public bool LevelEndSelected
{
get { return levelEndTickBox.Selected; }
set { levelEndTickBox.Selected = value; }
get { return levelEndTickBox?.Selected ?? levelEndSelected; }
set
{
TrySetTickBoxSelected(levelEndTickBox, ref levelEndSelected, value);
}
}
private bool maintainPos;
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)]
public bool MaintainPos
{
get { return maintainPosTickBox.Selected; }
set { maintainPosTickBox.Selected = value; }
get
{
return maintainPosTickBox?.Selected ?? maintainPos;
}
set
{
TrySetTickBoxSelected(maintainPosTickBox, ref maintainPos, value);
}
}
private float steerRadius;
@@ -554,7 +572,7 @@ namespace Barotrauma.Items.Components
int x = rect.X;
int y = rect.Y;
if (Voltage < MinVoltage) { return; }
if (!HasPower) { return; }
Rectangle velRect = new Rectangle(x + 20, y + 20, width - 40, height - 40);
Vector2 steeringOrigin = steerArea.Rect.Center.ToVector2();
@@ -759,7 +777,7 @@ namespace Barotrauma.Items.Components
dockingButton.Text = dockText;
}
if (Voltage < MinVoltage)
if (!HasPower)
{
tipContainer.Visible = true;
tipContainer.Text = noPowerTip;
@@ -829,7 +847,7 @@ namespace Barotrauma.Items.Components
}
if (!AutoPilot && Character.DisableControls && GUI.KeyboardDispatcher.Subscriber == null)
{
steeringAdjustSpeed = character == null ? DefaultSteeringAdjustSpeed : MathHelper.Lerp(0.2f, 1.0f, character.GetSkillLevel("helm") / 100.0f);
steeringAdjustSpeed = character == null ? DefaultSteeringAdjustSpeed : MathHelper.Lerp(0.2f, 1.0f, character.GetSkillLevel(Tags.HelmSkill) / 100.0f);
Vector2 input = Vector2.Zero;
if (PlayerInput.KeyDown(InputType.Left)) { input -= Vector2.UnitX; }
if (PlayerInput.KeyDown(InputType.Right)) { input += Vector2.UnitX; }
@@ -909,7 +927,7 @@ namespace Barotrauma.Items.Components
if (targetPort.Docked || targetPort.Item.Submarine == null) { continue; }
if (targetPort.Item.Submarine == controlledSub || targetPort.IsHorizontal != sourcePort.IsHorizontal) { continue; }
if (targetPort.Item.Submarine.DockedTo?.Contains(sourcePort.Item.Submarine) ?? false) { continue; }
if (Level.Loaded != null && targetPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; }
if (targetPort.Item.Submarine.IsAboveLevel) { continue; }
if (sourceDir == targetPort.GetDir()) { continue; }
float dist = Vector2.DistanceSquared(sourcePort.Item.WorldPosition, targetPort.Item.WorldPosition);
@@ -924,6 +942,21 @@ namespace Barotrauma.Items.Components
}
}
/// <summary>
/// Sets the value of the specified tickbox, or if it hasn't been instantiated (yet?), just the value of the backing field.
/// </summary>
private void TrySetTickBoxSelected(GUITickBox tickBox, ref bool backingValue, bool newValue)
{
if (tickBox == null)
{
backingValue = newValue;
}
else
{
tickBox.Selected = newValue;
}
}
private bool NudgeButtonClicked(GUIButton btn, object userdata)
{
if (!MaintainPos || !AutoPilot)

View File

@@ -50,7 +50,7 @@ namespace Barotrauma.Items.Components
Hull hull = Entity.FindEntityByID(hullID) as Hull;
item.Submarine = submarine;
item.CurrentHull = hull;
item.body.SetTransform(simPosition, item.body.Rotation);
item.body.SetTransformIgnoreContacts(simPosition, item.body.Rotation);
switch (targetType)
{
@@ -180,7 +180,11 @@ namespace Barotrauma.Items.Components
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "particleemitter":
particleEmitters.Add(new ParticleEmitter(subElement));
var emitter = new ParticleEmitter(subElement);
//backwards compatibility: previously it was not possible to change if the particles use tracer points, they were always used on projectiles
//now emitters don't use them by default, except on projectiles
emitter.Prefab.Properties.UseTracerPoints = subElement.GetAttributeBool(nameof(emitter.Prefab.Properties.UseTracerPoints), true);
particleEmitters.Add(emitter);
break;
}
}

View File

@@ -6,6 +6,7 @@ namespace Barotrauma.Items.Components
{
public override void DrawHUD(SpriteBatch spriteBatch, Character character)
{
base.DrawHUD(spriteBatch, character);
currentTarget?.DrawHUD(spriteBatch, Screen.Selected.Cam, character);
}

View File

@@ -58,7 +58,6 @@ namespace Barotrauma.Items.Components
}
}
partial void UseProjSpecific(float deltaTime, Vector2 raystart)
{
foreach (ParticleEmitter particleEmitter in particleEmitters)
@@ -88,25 +87,18 @@ namespace Barotrauma.Items.Components
MathUtils.InverseLerp(targetStructure.Prefab.MinHealth, targetStructure.Health, targetStructure.Health - targetStructure.SectionDamage(sectionIndex)),
GUIStyle.Red, GUIStyle.Green);
if (progressBar != null) progressBar.Size = new Vector2(60.0f, 20.0f);
Vector2 particlePos = ConvertUnits.ToDisplayUnits(pickedPosition);
if (targetStructure.Submarine != null) particlePos += targetStructure.Submarine.DrawPosition;
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
foreach (var emitter in particleEmitterHitStructure)
{
float particleAngle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi);
emitter.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi);
EmitParticle(emitter, deltaTime, pickedPosition, targetStructure.Submarine);
}
}
partial void FixCharacterProjSpecific(Character user, float deltaTime, Character targetCharacter)
{
Vector2 particlePos = ConvertUnits.ToDisplayUnits(pickedPosition);
if (targetCharacter.Submarine != null) particlePos += targetCharacter.Submarine.DrawPosition;
foreach (var emitter in particleEmitterHitCharacter)
{
float particleAngle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi);
emitter.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi);
EmitParticle(emitter, deltaTime, pickedPosition, targetCharacter.Submarine);
}
}
@@ -134,15 +126,22 @@ namespace Barotrauma.Items.Components
}
}
Vector2 particlePos = ConvertUnits.ToDisplayUnits(pickedPosition);
if (targetItem.Submarine != null) particlePos += targetItem.Submarine.DrawPosition;
foreach ((RelatedItem relatedItem, ParticleEmitter emitter) in particleEmitterHitItem)
{
if (!relatedItem.MatchesItem(targetItem)) { continue; }
float particleAngle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi);
emitter.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi);
EmitParticle(emitter, deltaTime, pickedPosition, targetItem.Submarine);
}
}
private void EmitParticle(ParticleEmitter emitter, float deltaTime, Vector2 simPosition, Submarine targetSub)
{
Vector2 particlePos = ConvertUnits.ToDisplayUnits(simPosition);
if (targetSub != null) { particlePos += targetSub.DrawPosition; }
float particleAngle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi);
emitter.Emit(deltaTime, particlePos, item.CurrentHull, particleAngle + MathHelper.Pi, -particleAngle + MathHelper.Pi,
tracerPoints: new Tuple<Vector2, Vector2>(item.WorldPosition + TransformedBarrelPos, particlePos));
}
#if DEBUG
public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null)
{

View File

@@ -299,6 +299,8 @@ namespace Barotrauma.Items.Components
public override void DrawHUD(SpriteBatch spriteBatch, Character character)
{
base.DrawHUD(spriteBatch, character);
IsActive = true;
float defaultMaxCondition = (item.MaxCondition / item.MaxRepairConditionMultiplier);

View File

@@ -52,9 +52,11 @@ namespace Barotrauma.Items.Components
Vector2 sourcePos = GetSourcePos();
//need to double the size because this is essentially just the radius, we need the diameter
// + some extra margin to be on the safe side
return new Vector2(
Math.Abs(target.DrawPosition.X - sourcePos.X),
Math.Abs(target.DrawPosition.Y - sourcePos.Y)) * 1.5f;
Math.Abs(target.DrawPosition.Y - sourcePos.Y)) * 2.2f;
}
}
@@ -122,7 +124,7 @@ namespace Barotrauma.Items.Components
{
if (turret.BarrelSprite != null)
{
startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f;
startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * turret.Item.Scale * BarrelLengthMultiplier;
}
startPos -= turret.GetRecoilOffset();
}
@@ -227,7 +229,7 @@ namespace Barotrauma.Items.Components
{
if (reelSoundChannel is not { IsPlaying: true })
{
reelSoundChannel = SoundPlayer.PlaySound(sound.Sound, position, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier());
reelSoundChannel = SoundPlayer.PlaySound(sound, position);
if (reelSoundChannel != null)
{
reelSoundChannel.Looping = true;
@@ -242,7 +244,7 @@ namespace Barotrauma.Items.Components
}
else
{
SoundPlayer.PlaySound(sound.Sound, position, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier());
SoundPlayer.PlaySound(sound, position);
}
}

View File

@@ -3,7 +3,6 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -16,7 +15,7 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific(ContentXElement element)
{
terminalButtonStyles = new string[RequiredSignalCount];
terminalButtonStyles = new string[requiredSignalCount];
int i = 0;
foreach (var childElement in element.GetChildElements("TerminalButton"))
{
@@ -38,11 +37,11 @@ namespace Barotrauma.Items.Components
};
paddedFrame.OnAddedToGUIUpdateList += (component) =>
{
bool buttonsEnabled = AllowUsingButtons;
foreach (var child in component.Children)
bool buttonsEnabled = IsActivated;
foreach (GUIComponent child in component.Children)
{
if (!(child is GUIButton)) { continue; }
if (!(child.UserData is int)) { continue; }
if (child is not GUIButton) { continue; }
if (child.UserData is not int) { continue; }
child.Enabled = buttonsEnabled;
child.Children.ForEach(c => c.Enabled = buttonsEnabled);
}
@@ -59,7 +58,7 @@ namespace Barotrauma.Items.Components
containerIndicator.OverrideState = itemsContained ? GUIComponent.ComponentState.Selected : GUIComponent.ComponentState.None;
};
float x = 1.0f / (1 + RequiredSignalCount);
float x = 1.0f / (1 + requiredSignalCount);
float y = Math.Min((x * paddedFrame.Rect.Width) / paddedFrame.Rect.Height, 0.5f);
Vector2 relativeSize = new Vector2(x, y);
@@ -69,7 +68,7 @@ namespace Barotrauma.Items.Components
containerIndicator = new GUIImage(new RectTransform(new Vector2(0.5f, 0.5f * (1.0f - y)), containerSection.RectTransform, anchor: Anchor.BottomCenter),
style: "IndicatorLightRed", scaleToFit: true);
for (int i = 0; i < RequiredSignalCount; i++)
for (int i = 0; i < requiredSignalCount; i++)
{
var button = new GUIButton(new RectTransform(relativeSize, paddedFrame.RectTransform), style: null)
{
@@ -111,7 +110,7 @@ namespace Barotrauma.Items.Components
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), sender: null, isServerMessage: true);
SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), sender: null, ignoreState: true);
}
}
}

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
@@ -129,7 +129,7 @@ namespace Barotrauma.Items.Components
public void RemoveComponents(IReadOnlyCollection<CircuitBoxComponent> node)
{
if (Locked) { return; }
if (IsLocked()) { return; }
var ids = node.Select(static n => n.ID).ToImmutableArray();
if (GameMain.NetworkMember is null)
@@ -146,7 +146,7 @@ namespace Barotrauma.Items.Components
public void AddWire(CircuitBoxConnection one, CircuitBoxConnection two)
{
if (Locked) { return; }
if (IsLocked()) { return; }
if (GameMain.NetworkMember is null)
{
Connect(one, two, static delegate { }, CircuitBoxWire.SelectedWirePrefab);
@@ -160,7 +160,7 @@ namespace Barotrauma.Items.Components
public void RemoveWires(IReadOnlyCollection<CircuitBoxWire> wires)
{
if (Locked) { return; }
if (IsLocked()) { return; }
var ids = wires.Select(static w => w.ID).ToImmutableArray();
if (GameMain.NetworkMember is null)
{
@@ -230,7 +230,7 @@ namespace Barotrauma.Items.Components
public void MoveComponent(Vector2 moveAmount, IReadOnlyCollection<CircuitBoxNode> moveables)
{
if (Locked) { return; }
if (IsLocked()) { return; }
var ids = ImmutableArray.CreateBuilder<ushort>();
var ios = ImmutableArray.CreateBuilder<CircuitBoxInputOutputNode.Type>();
var labelIds = ImmutableArray.CreateBuilder<ushort>();
@@ -265,7 +265,7 @@ namespace Barotrauma.Items.Components
public void AddComponent(ItemPrefab prefab, Vector2 pos)
{
if (Locked) { return; }
if (IsLocked()) { return; }
if (GameMain.NetworkMember is null)
{
ItemPrefab resource;
@@ -292,7 +292,7 @@ namespace Barotrauma.Items.Components
public void RenameLabel(CircuitBoxLabelNode label, Color color, NetLimitedString header, NetLimitedString body)
{
if (Locked) { return; }
if (IsLocked()) { return; }
if (GameMain.NetworkMember is null)
{
label.EditText(header, body);
@@ -316,7 +316,7 @@ namespace Barotrauma.Items.Components
public void ResizeNode(CircuitBoxNode node, CircuitBoxResizeDirection dir, Vector2 amount)
{
if (Locked) { return; }
if (IsLocked()) { return; }
var resize = node.ResizeBy(dir, amount);
if (GameMain.NetworkMember is null)
{
@@ -341,7 +341,7 @@ namespace Barotrauma.Items.Components
public void AddLabel(Vector2 pos)
{
if (Locked) { return; }
if (IsLocked()) { return; }
if (GameMain.NetworkMember is null)
{
AddLabelInternal(ICircuitBoxIdentifiable.FindFreeID(Labels), GUIStyle.Blue, pos, CircuitBoxLabelNode.DefaultHeaderText, NetLimitedString.Empty);
@@ -353,7 +353,7 @@ namespace Barotrauma.Items.Components
public void RemoveLabel(IReadOnlyCollection<CircuitBoxLabelNode> labels)
{
if (Locked) { return; }
if (IsLocked()) { return; }
if (!labels.Any()) { return; }
var ids = labels.Select(static n => n.ID).ToImmutableArray();

View File

@@ -12,7 +12,9 @@ namespace Barotrauma.Items.Components
private readonly List<GUIComponent> uiElements = new List<GUIComponent>();
private GUILayoutGroup uiElementContainer;
private bool readingNetworkEvent;
private bool suppressNetworkEvents;
private GUIComponent insufficientPowerWarning;
private Point ElementMaxSize => new Point(uiElementContainer.Rect.Width, (int)(65 * GUI.yScale));
@@ -40,7 +42,7 @@ namespace Barotrauma.Items.Components
float elementSize = Math.Min(1.0f / visibleElements.Count(), 1);
foreach (CustomInterfaceElement ciElement in visibleElements)
{
if (ciElement.HasPropertyName)
if (ciElement.InputType is CustomInterfaceElement.InputTypeOption.Number or CustomInterfaceElement.InputTypeOption.Text)
{
var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform), isHorizontal: true)
{
@@ -49,7 +51,7 @@ namespace Barotrauma.Items.Components
};
new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform),
TextManager.Get(ciElement.Label).Fallback(ciElement.Label));
if (!ciElement.IsNumberInput)
if (ciElement.InputType is CustomInterfaceElement.InputTypeOption.Text)
{
var textBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), ciElement.Signal, style: "GUITextBoxNoIcon")
{
@@ -68,7 +70,7 @@ namespace Barotrauma.Items.Components
}
else
{
item.CreateClientEvent(this);
CreateClientEventWithCorrectionDelay();
}
};
@@ -98,13 +100,10 @@ namespace Barotrauma.Items.Components
ValueStep = numberInputStep,
OnValueChanged = (ni) =>
{
if (GameMain.Client == null)
ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue);
if (!suppressNetworkEvents && GameMain.Client != null)
{
ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue);
}
else if (!readingNetworkEvent)
{
item.CreateClientEvent(this);
CreateClientEventWithCorrectionDelay();
}
}
};
@@ -124,13 +123,10 @@ namespace Barotrauma.Items.Components
ValueStep = numberInputStep,
OnValueChanged = (ni) =>
{
if (GameMain.Client == null)
ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue);
if (!suppressNetworkEvents && GameMain.Client != null)
{
ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue);
}
else if (!readingNetworkEvent)
{
item.CreateClientEvent(this);
CreateClientEventWithCorrectionDelay();
}
}
};
@@ -149,7 +145,7 @@ namespace Barotrauma.Items.Components
}
}
}
else if (ciElement.ContinuousSignal)
else if (ciElement.InputType is CustomInterfaceElement.InputTypeOption.TickBox)
{
var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform)
{
@@ -160,13 +156,10 @@ namespace Barotrauma.Items.Components
};
tickBox.OnSelected += (tBox) =>
{
if (GameMain.Client == null)
TickBoxToggled(tBox.UserData as CustomInterfaceElement, tBox.Selected);
if (!suppressNetworkEvents && GameMain.Client != null)
{
TickBoxToggled(tBox.UserData as CustomInterfaceElement, tBox.Selected);
}
else if (!readingNetworkEvent)
{
item.CreateClientEvent(this);
CreateClientEventWithCorrectionDelay();
}
return true;
};
@@ -175,7 +168,7 @@ namespace Barotrauma.Items.Components
tickBox.RectTransform.MaxSize = new Point(int.MaxValue, int.MaxValue);
uiElements.Add(tickBox);
}
else
else if (ciElement.InputType is CustomInterfaceElement.InputTypeOption.Button)
{
var btn = new GUIButton(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform),
TextManager.Get(ciElement.Label).Fallback(ciElement.Label), style: "DeviceButton")
@@ -189,8 +182,10 @@ namespace Barotrauma.Items.Components
{
ButtonClicked(btnElement);
}
else if (!readingNetworkEvent)
else if (!suppressNetworkEvents && GameMain.Client != null)
{
//don't use CreateClientEventWithCorrectionDelay here, because buttons have no state,
//which means we don't need to worry about server updates interfering with client-side changes to the values in the interface
item.CreateClientEvent(this, new EventData(btnElement));
}
return true;
@@ -203,6 +198,22 @@ namespace Barotrauma.Items.Components
uiElements.Add(btn);
}
}
if (ShowInsufficientPowerWarning)
{
insufficientPowerWarning = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), GuiFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { MinSize = new Point(0, GUI.IntScale(30)) },
TextManager.Get("SteeringNoPowerTip"), font: GUIStyle.Font, wrap: true, style: "GUIToolTip", textAlignment: Alignment.Center)
{
AutoScaleHorizontal = true,
Visible = false
};
}
void CreateClientEventWithCorrectionDelay()
{
item.CreateClientEvent(this);
correctionTimer = CorrectionDelay;
}
}
public override void CreateEditingHUD(SerializableEntityEditor editor)
@@ -253,7 +264,20 @@ namespace Barotrauma.Items.Components
{
if (uiElement.UserData is not CustomInterfaceElement element) { continue; }
bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Count > 0);
if (visible) { visibleElementCount++; }
if (visible)
{
visibleElementCount++;
if (element.GetValueInterval > 0.0f && correctionTimer <= 0.0f)
{
element.GetValueTimer -= deltaTime;
if (element.GetValueTimer <= 0.0f)
{
SetSignalToPropertyValue(element);
UpdateSignalProjSpecific(uiElement);
element.GetValueTimer = element.GetValueInterval;
}
}
}
if (uiElement.Visible != visible)
{
uiElement.Visible = visible;
@@ -274,6 +298,11 @@ namespace Barotrauma.Items.Components
GuiFrame.Visible = visibleElementCount > 0;
uiElementContainer.Recalculate();
}
if (insufficientPowerWarning != null)
{
insufficientPowerWarning.Visible = item.GetComponents<Powered>().Any(p => p.PowerConsumption > 0.0f && p.Voltage < p.MinVoltage);
}
}
partial void UpdateLabelsProjSpecific()
@@ -300,7 +329,7 @@ namespace Barotrauma.Items.Components
LocalizedString CreateLabelText(int elementIndex)
{
var label = customInterfaceElementList[elementIndex].Label;
string label = customInterfaceElementList[elementIndex].Label;
return string.IsNullOrWhiteSpace(label) ?
TextManager.GetWithVariable("connection.signaloutx", "[num]", (elementIndex + 1).ToString()) :
TextManager.Get(label).Fallback(label);
@@ -336,22 +365,43 @@ namespace Barotrauma.Items.Components
if (signals == null) { return; }
for (int i = 0; i < signals.Length && i < uiElements.Count; i++)
{
string signal = customInterfaceElementList[i].Signal;
if (uiElements[i] is GUITextBox tb)
UpdateSignalProjSpecific(uiElements[i]);
}
}
private void UpdateSignalProjSpecific(GUIComponent uiElement)
{
if (uiElement.UserData is not CustomInterfaceElement element) { return; }
suppressNetworkEvents = true;
string signal = element.Signal;
if (uiElement is GUITextBox tb)
{
tb.Text = Screen.Selected is { IsEditor: true } ?
signal :
TextManager.Get(signal).Fallback(signal).Value;
}
else if (uiElement is GUINumberInput ni)
{
if (ni.InputType == NumberType.Int)
{
tb.Text = Screen.Selected is { IsEditor: true } ?
signal :
TextManager.Get(signal).Fallback(signal).Value;
}
else if (uiElements[i] is GUINumberInput ni)
{
if (ni.InputType == NumberType.Int)
if (int.TryParse(signal, out int value))
{
int.TryParse(signal, out int value);
ni.IntValue = value;
}
else if (float.TryParse(signal, out float floatValue))
{
ni.IntValue = (int)MathF.Round(floatValue);
}
}
}
else if (uiElement is GUITickBox tickBox)
{
tickBox.Selected = signal.Equals("true", StringComparison.OrdinalIgnoreCase);
}
suppressNetworkEvents = false;
}
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
@@ -360,14 +410,9 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
var element = customInterfaceElementList[i];
if (element.HasPropertyName)
switch (element.InputType)
{
if (!element.IsNumberInput)
{
msg.WriteString(((GUITextBox)uiElements[i]).Text);
}
else
{
case CustomInterfaceElement.InputTypeOption.Number:
switch (element.NumberType)
{
case NumberType.Float:
@@ -378,59 +423,82 @@ namespace Barotrauma.Items.Components
msg.WriteString(((GUINumberInput)uiElements[i]).IntValue.ToString());
break;
}
}
}
else if (element.ContinuousSignal)
{
msg.WriteBoolean(((GUITickBox)uiElements[i]).Selected);
}
else
{
msg.WriteBoolean(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]);
break;
case CustomInterfaceElement.InputTypeOption.Text:
msg.WriteString(((GUITextBox)uiElements[i]).Text);
break;
case CustomInterfaceElement.InputTypeOption.TickBox:
msg.WriteBoolean(((GUITickBox)uiElements[i]).Selected);
break;
case CustomInterfaceElement.InputTypeOption.Button:
msg.WriteBoolean(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]);
break;
}
}
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
readingNetworkEvent = true;
int msgStartPos = msg.BitPosition;
suppressNetworkEvents = true;
try
{
string[] stringValues = new string[customInterfaceElementList.Count];
bool[] boolValues = new bool[customInterfaceElementList.Count];
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
var element = customInterfaceElementList[i];
if (element.HasPropertyName)
switch (element.InputType)
{
string newValue = msg.ReadString();
if (!element.IsNumberInput)
{
TextChanged(element, newValue);
}
else
{
case CustomInterfaceElement.InputTypeOption.Number:
case CustomInterfaceElement.InputTypeOption.Text:
stringValues[i] = msg.ReadString();
break;
case CustomInterfaceElement.InputTypeOption.TickBox:
case CustomInterfaceElement.InputTypeOption.Button:
boolValues[i] = msg.ReadBoolean();
break;
}
}
if (correctionTimer > 0.0f)
{
int msgLength = msg.BitPosition - msgStartPos;
msg.BitPosition = msgStartPos;
StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime);
return;
}
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
var element = customInterfaceElementList[i];
switch (element.InputType)
{
case CustomInterfaceElement.InputTypeOption.Number:
switch (element.NumberType)
{
case NumberType.Int when int.TryParse(newValue, out int value):
case NumberType.Int when int.TryParse(stringValues[i], out int value):
ValueChanged(element, value);
break;
case NumberType.Float when TryParseFloatInvariantCulture(newValue, out float value):
case NumberType.Float when TryParseFloatInvariantCulture(stringValues[i], out float value):
ValueChanged(element, value);
break;
}
}
}
else
{
bool elementState = msg.ReadBoolean();
if (element.ContinuousSignal)
{
((GUITickBox)uiElements[i]).Selected = elementState;
TickBoxToggled(element, elementState);
}
else if (elementState)
{
ButtonClicked(element);
}
break;
case CustomInterfaceElement.InputTypeOption.Text:
TextChanged(element, stringValues[i]);
break;
case CustomInterfaceElement.InputTypeOption.TickBox:
bool tickBoxState = boolValues[i];
((GUITickBox)uiElements[i]).Selected = tickBoxState;
TickBoxToggled(element, tickBoxState);
break;
case CustomInterfaceElement.InputTypeOption.Button:
if (boolValues[i])
{
ButtonClicked(element);
}
break;
}
}
@@ -438,7 +506,7 @@ namespace Barotrauma.Items.Components
}
finally
{
readingNetworkEvent = false;
suppressNetworkEvents = false;
}
}
}

View File

@@ -1,6 +1,7 @@
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -21,13 +22,16 @@ namespace Barotrauma.Items.Components
private GUIListBox historyBox;
private GUITextBlock fillerBlock;
private GUITextBox inputBox;
private GUILayoutGroup layoutGroup;
private bool shouldSelectInputBox;
private readonly List<GUIComponent> inputElements = new List<GUIComponent>();
partial void InitProjSpecific(XElement element)
{
float marginMultiplier = element.GetAttributeFloat("marginmultiplier", 1.0f);
var layoutGroup = new GUILayoutGroup(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin.Multiply(marginMultiplier), GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset.Multiply(marginMultiplier) })
layoutGroup = new GUILayoutGroup(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin.Multiply(marginMultiplier), GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset.Multiply(marginMultiplier) })
{
ChildAnchor = Anchor.TopCenter,
RelativeSpacing = 0.02f,
@@ -39,43 +43,53 @@ namespace Barotrauma.Items.Components
AutoHideScrollBar = this.AutoHideScrollbar
};
if (!Readonly)
inputElements.Add(CreateFillerBlock());
inputElements.Add(new GUIFrame(new RectTransform(new Vector2(0.9f, 0.01f), layoutGroup.RectTransform), style: "HorizontalLine"));
inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: TextColor)
{
CreateFillerBlock();
new GUIFrame(new RectTransform(new Vector2(0.9f, 0.01f), layoutGroup.RectTransform), style: "HorizontalLine");
inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: TextColor)
MaxTextLength = MaxMessageLength,
OverflowClip = true,
OnEnterPressed = (GUITextBox textBox, string text) =>
{
MaxTextLength = MaxMessageLength,
OverflowClip = true,
OnEnterPressed = (GUITextBox textBox, string text) =>
if (GameMain.NetworkMember == null)
{
if (GameMain.NetworkMember == null)
{
SendOutput(text);
}
else
{
item.CreateClientEvent(this, new ClientEventData(text));
}
textBox.Text = string.Empty;
return true;
SendOutput(text);
}
};
}
else
{
item.CreateClientEvent(this, new ClientEventData(text));
}
textBox.Text = string.Empty;
return true;
}
};
inputElements.Add(inputBox);
RefreshInputElements();
}
layoutGroup.Recalculate();
/// <summary>
/// Refreshes the visibility of the input box and the layout of the UI depending on whether the terminal is readonly or not.
/// </summary>
private void RefreshInputElements()
{
foreach (var inputElement in inputElements)
{
inputElement.Visible = !_readonly;
inputElement.IgnoreLayoutGroups = !inputElement.Visible;
}
layoutGroup?.Recalculate();
}
// Create fillerBlock to cover historyBox so new values appear at the bottom of historyBox
// This could be removed if GUIListBox supported aligning its children
public void CreateFillerBlock()
public GUIComponent CreateFillerBlock()
{
fillerBlock = new GUITextBlock(new RectTransform(new Vector2(1, 1), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), string.Empty)
{
CanBeFocused = false
};
return fillerBlock;
}
private void SendOutput(string input)

View File

@@ -21,9 +21,14 @@ namespace Barotrauma.Items.Components
ShapeExtensions.DrawCircle(spriteBatch, pos, range, 32, Color.Cyan * 0.5f, 3);
}
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
{
SharedEventWrite(msg);
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
Channel = msg.ReadRangedInteger(MinChannel, MaxChannel);
SharedEventRead(msg);
}
}
}

View File

@@ -106,13 +106,6 @@ namespace Barotrauma.Items.Components
private static int? selectedNodeIndex;
private static int? highlightedNodeIndex;
[Serialize(0.3f, IsPropertySaveable.No)]
public float Width
{
get;
set;
}
public Vector2 DrawSize
{
get { return sectionExtents; }
@@ -199,6 +192,8 @@ namespace Barotrauma.Items.Components
return;
}
if (Width * wireSprite.size.Y * Screen.Selected.Cam.Zoom < 1.0f) { return; }
Vector2 drawOffset = GetDrawOffset() + offset;
float baseDepth = UseSpriteDepth ? item.SpriteDepth : wireSprite.Depth;

View File

@@ -175,6 +175,8 @@ namespace Barotrauma.Items.Components
{
if (character == null) { return; }
base.DrawHUD(spriteBatch, character);
if (OverlayColor.A > 0)
{
GUIStyle.UIGlow.Draw(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight),
@@ -206,44 +208,51 @@ namespace Barotrauma.Items.Components
if (ThermalGoggles)
{
spriteBatch.End();
GameMain.LightManager.SolidColorEffect.Parameters["color"].SetValue(Color.Red.ToVector4() * (0.3f + MathF.Sin(thermalEffectState) * 0.05f));
GameMain.LightManager.SolidColorEffect.CurrentTechnique = GameMain.LightManager.SolidColorEffect.Techniques["SolidColorBlur"];
GameMain.LightManager.SolidColorEffect.Parameters["blurDistance"].SetValue(0.01f + MathF.Sin(thermalEffectState) * 0.005f);
GameMain.LightManager.SolidColorEffect.CurrentTechnique.Passes[0].Apply();
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: Screen.Selected.Cam.Transform, effect: GameMain.LightManager.SolidColorEffect);
Entity refEntity = equipper;
if (!isEquippable || refEntity == null)
{
refEntity = item;
}
DrawThermalOverlay(spriteBatch, refEntity, character, OverlayColor, Range, thermalEffectState, ShowDeadCharacters);
foreach (Character c in Character.CharacterList)
{
if (c == character || !c.Enabled || c.Removed || c.Params.HideInThermalGoggles) { continue; }
if (!ShowDeadCharacters && c.IsDead) { continue; }
float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition);
if (dist > Range * Range) { continue; }
Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite;
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.Mass < 0.5f && limb != c.AnimController.MainLimb) { continue; }
float noise1 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.02f);
float noise2 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.008f);
Vector2 spriteScale = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) / pingCircle.size * (noise1 * 0.5f + 2f);
Vector2 drawPos = new Vector2(limb.body.DrawPosition.X + (noise1 - 0.5f) * 100, -limb.body.DrawPosition.Y + (noise2 - 0.5f) * 100);
pingCircle.Draw(spriteBatch, drawPos, 0.0f, scale: Math.Max(spriteScale.X, spriteScale.Y));
}
}
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied);
}
}
public static void DrawThermalOverlay(SpriteBatch spriteBatch, Entity refEntity, Character user, Color overlayColor, float range, float effectState, bool showDeadCharacters)
{
spriteBatch.End();
float colorIntensityBase = 0.5f; //Multiplies the overlay color by this amount, the higher the value, the more bright/vibrant the color.
float colorIntensityVariance = 0.05f; //The variance of the pulse effect affecting the color's brightness/vibrance
GameMain.LightManager.SolidColorEffect.Parameters["color"].SetValue(overlayColor.ToVector4() * (colorIntensityBase + MathF.Sin(effectState) * colorIntensityVariance));
GameMain.LightManager.SolidColorEffect.CurrentTechnique = GameMain.LightManager.SolidColorEffect.Techniques["SolidColorBlur"];
GameMain.LightManager.SolidColorEffect.Parameters["blurDistance"].SetValue(0.01f + MathF.Sin(effectState) * 0.005f);
GameMain.LightManager.SolidColorEffect.CurrentTechnique.Passes[0].Apply();
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: Screen.Selected.Cam.Transform, effect: GameMain.LightManager.SolidColorEffect);
foreach (Character c in Character.CharacterList)
{
if (c == user || !c.Enabled || c.Removed || c.Params.HideInThermalGoggles) { continue; }
if (!showDeadCharacters && c.IsDead) { continue; }
float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition);
if (dist > range * range) { continue; }
Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite;
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.Mass < 0.5f && limb != c.AnimController.MainLimb) { continue; }
float noise1 = PerlinNoise.GetPerlin((effectState + limb.Params.ID + c.ID) * 0.01f, (effectState + limb.Params.ID + c.ID) * 0.02f);
float noise2 = PerlinNoise.GetPerlin((effectState + limb.Params.ID + c.ID) * 0.01f, (effectState + limb.Params.ID + c.ID) * 0.008f);
Vector2 spriteScale = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) / pingCircle.size * (noise1 * 0.5f + 2f);
Vector2 drawPos = new Vector2(limb.body.DrawPosition.X + (noise1 - 0.5f) * 100, -limb.body.DrawPosition.Y + (noise2 - 0.5f) * 100);
pingCircle.Draw(spriteBatch, drawPos, 0.0f, scale: Math.Max(spriteScale.X, spriteScale.Y));
}
}
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied);
}
private void DrawCharacterInfo(SpriteBatch spriteBatch, Character target, float alpha = 1.0f)
{
Vector2 hudPos = GameMain.GameScreen.Cam.WorldToScreen(target.DrawPosition);
@@ -273,7 +282,7 @@ namespace Barotrauma.Items.Components
}
else
{
if (!target.CustomInteractHUDText.IsNullOrEmpty() && target.AllowCustomInteract)
if (target.ShouldShowCustomInteractText)
{
texts.Add(target.CustomInteractHUDText);
textColors.Add(GUIStyle.Green);

View File

@@ -1,12 +1,28 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
namespace Barotrauma.Items.Components
{
partial class TriggerComponent : ItemComponent, IServerSerializable
partial class TriggerComponent : ItemComponent, IServerSerializable, IDrawableComponent
{
public Vector2 DrawSize =>
Vector2.One *
(Radius > 0.0f ? Radius * 2 : Math.Max(Width, Height));
public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null)
{
if (editing)
{
PhysicsBody.DebugDraw(spriteBatch, Color.LightGray * 0.7f);
}
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
CurrentForceFluctuation = msg.ReadRangedSingle(0.0f, 1.0f, 8);
}
}
}

View File

@@ -92,16 +92,17 @@ namespace Barotrauma.Items.Components
get
{
float size = Math.Max(transformedBarrelPos.X, transformedBarrelPos.Y);
if (barrelSprite != null)
if (railSprite != null && barrelSprite != null)
{
if (railSprite != null)
{
size += Math.Max(Math.Max(barrelSprite.size.X, barrelSprite.size.Y), Math.Max(railSprite.size.X, railSprite.size.Y)) * item.Scale;
}
else
{
size += Math.Max(barrelSprite.size.X, barrelSprite.size.Y) * item.Scale;
}
size += Math.Max(Math.Max(barrelSprite.size.X, barrelSprite.size.Y), Math.Max(railSprite.size.X, railSprite.size.Y)) * item.Scale;
}
else if (railSprite != null)
{
size += Math.Max(railSprite.size.X, railSprite.size.Y) * item.Scale;
}
else if (barrelSprite != null)
{
size += Math.Max(barrelSprite.size.X, barrelSprite.size.Y) * item.Scale;
}
return Vector2.One * size * 2;
}
@@ -227,14 +228,14 @@ namespace Barotrauma.Items.Components
{
if (moveSoundChannel == null && startMoveSound != null)
{
moveSoundChannel = SoundPlayer.PlaySound(startMoveSound.Sound, item.WorldPosition, startMoveSound.Volume, startMoveSound.Range, ignoreMuffling: startMoveSound.IgnoreMuffling, freqMult: startMoveSound.GetRandomFrequencyMultiplier());
moveSoundChannel = SoundPlayer.PlaySound(startMoveSound, item.WorldPosition, hullGuess: item.CurrentHull);
}
else if (moveSoundChannel == null || !moveSoundChannel.IsPlaying)
{
if (moveSound != null)
{
moveSoundChannel?.FadeOutAndDispose();
moveSoundChannel = SoundPlayer.PlaySound(moveSound.Sound, item.WorldPosition, moveSound.Volume, moveSound.Range, ignoreMuffling: moveSound.IgnoreMuffling, freqMult: moveSound.GetRandomFrequencyMultiplier());
moveSoundChannel = SoundPlayer.PlaySound(moveSound, item.WorldPosition, hullGuess: item.CurrentHull);
if (moveSoundChannel != null) { moveSoundChannel.Looping = true;}
}
}
@@ -246,7 +247,7 @@ namespace Barotrauma.Items.Components
if (endMoveSound != null && moveSoundChannel.Sound != endMoveSound.Sound)
{
moveSoundChannel.FadeOutAndDispose();
moveSoundChannel = SoundPlayer.PlaySound(endMoveSound.Sound, item.WorldPosition, endMoveSound.Volume, endMoveSound.Range, ignoreMuffling: endMoveSound.IgnoreMuffling, freqMult: endMoveSound.GetRandomFrequencyMultiplier());
moveSoundChannel = SoundPlayer.PlaySound(endMoveSound, item.WorldPosition, hullGuess: item.CurrentHull);
if (moveSoundChannel != null) { moveSoundChannel.Looping = false; }
}
else if (!moveSoundChannel.IsPlaying)
@@ -275,7 +276,7 @@ namespace Barotrauma.Items.Components
{
if (chargeSound != null)
{
chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling, freqMult: chargeSound.GetRandomFrequencyMultiplier());
chargeSoundChannel = SoundPlayer.PlaySound(chargeSound, item.WorldPosition, hullGuess: item.CurrentHull);
if (chargeSoundChannel != null) { chargeSoundChannel.Looping = true; }
}
}
@@ -385,17 +386,20 @@ namespace Barotrauma.Items.Components
if (item.Condition > 0.0f || !HideBarrelWhenBroken)
{
railSprite?.Draw(spriteBatch,
var currentRailSprite = item.Condition <= 0.0f && railSpriteBroken != null ? railSpriteBroken : railSprite;
var currentBarrelSprite = item.Condition <= 0.0f && barrelSpriteBroken != null ? barrelSpriteBroken : barrelSprite;
currentRailSprite?.Draw(spriteBatch,
drawPos,
overrideColor ?? item.SpriteColor,
Rotation + MathHelper.PiOver2, item.Scale,
SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth));
SpriteEffects.None, item.SpriteDepth + (currentRailSprite.Depth - item.Sprite.Depth));
barrelSprite?.Draw(spriteBatch,
currentBarrelSprite?.Draw(spriteBatch,
drawPos - GetRecoilOffset() * item.Scale,
overrideColor ?? item.SpriteColor,
Rotation + MathHelper.PiOver2, item.Scale,
SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth));
SpriteEffects.None, item.SpriteDepth + (currentBarrelSprite.Depth - item.Sprite.Depth));
float chargeRatio = currentChargeTime / MaxChargeTime;
@@ -702,6 +706,8 @@ namespace Barotrauma.Items.Components
public override void DrawHUD(SpriteBatch spriteBatch, Character character)
{
base.DrawHUD(spriteBatch, character);
if (HudTint.A > 0)
{
GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight),

View File

@@ -1359,7 +1359,7 @@ namespace Barotrauma
if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container &&
container.Inventory.CanBePut(item))
{
if (!container.AllowDragAndDrop || !container.AllowAccess)
if (!container.AllowDragAndDrop || !container.IsAccessible())
{
allowCombine = false;
}
@@ -1603,7 +1603,8 @@ namespace Barotrauma
shadowSprite.Draw(spriteBatch,
new Rectangle(itemPos.ToPoint() - new Point((iconSize / 2 - shadowPadding.X) * textDir - shadowSize.X * textOffset, iconSize / 2 + shadowPadding.Y), shadowSize), Color.Black * 0.8f);
GUI.DrawString(spriteBatch, textPos + new Vector2(nameSize.X * textOffset, -iconSize / 2), DraggingItems.First().Name, Color.White);
var richString = RichString.Rich(DraggingItems.First().Name);
GUI.DrawStringWithColors(spriteBatch, textPos + new Vector2(nameSize.X * textOffset, -iconSize / 2), richString.SanitizedValue, Color.White, richString.RichTextData);
GUI.DrawString(spriteBatch, textPos + new Vector2(toolTipSize.X * textOffset, 0), toolTip,
color: toolTipColor,
font: GUIStyle.SmallFont);

View File

@@ -130,7 +130,7 @@ namespace Barotrauma
}
}
public float GetDrawDepth()
public override float GetDrawDepth()
{
return GetDrawDepth(SpriteDepth + DrawDepthOffset, Sprite);
}
@@ -287,7 +287,7 @@ namespace Barotrauma
}
else
{
int padding = 100;
int padding = 0;
RectangleF boundingBox = GetTransformedQuad().BoundingAxisAlignedRectangle;
Vector2 min = new Vector2(-boundingBox.Width / 2 - padding, -boundingBox.Height / 2 - padding);
@@ -302,11 +302,11 @@ namespace Barotrauma
}
foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites)
{
float scale = decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale;
min.X = Math.Min(-decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * scale, min.X);
min.Y = Math.Min(-decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale, min.Y);
max.X = Math.Max(decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * scale, max.X);
max.Y = Math.Max(decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale, max.Y);
Vector2 scale = decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale;
min.X = Math.Min(-decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * scale.X, min.X);
min.Y = Math.Min(-decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale.Y, min.Y);
max.X = Math.Max(decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * scale.X, max.X);
max.Y = Math.Max(decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale.Y, max.Y);
}
cachedVisibleExtents = extents = new Rectangle(min.ToPoint(), max.ToPoint());
}
@@ -316,6 +316,9 @@ namespace Barotrauma
if (worldPosition.X + extents.X > worldView.Right || worldPosition.X + extents.Width < worldView.X) { return false; }
if (worldPosition.Y + extents.Height < worldView.Y - worldView.Height || worldPosition.Y + extents.Y > worldView.Y) { return false; }
if (extents.Width * Screen.Selected.Cam.Zoom < 1.0f) { return false; }
if (extents.Height * Screen.Selected.Cam.Zoom < 1.0f) { return false; }
return true;
}
@@ -405,7 +408,7 @@ namespace Barotrauma
fadeInBrokenSprite.Sprite.effects ^= SpriteEffects;
}
if (body == null)
if (body == null || body.BodyType == BodyType.Static)
{
if (Prefab.ResizeHorizontal || Prefab.ResizeVertical)
{
@@ -490,7 +493,7 @@ namespace Barotrauma
}
}
var head = holdable.Picker.AnimController.GetLimb(LimbType.Head);
if (head != null)
if (head?.Sprite != null)
{
//ensure the holdable item is always drawn in front of the head no matter what the wearables or whatnot do with the sprite depths
depth =
@@ -523,8 +526,8 @@ namespace Barotrauma
Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale;
if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; }
if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; }
decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color,
rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects,
decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, decorativeSprite.Sprite.Origin,
rotation, decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects,
depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth));
}
}
@@ -673,14 +676,8 @@ namespace Barotrauma
origin.Y = -origin.Y + decorativeSprite.Sprite.size.Y;
spriteEffects |= SpriteEffects.FlipVertically;
}
if (body != null)
{
var ca = MathF.Cos(-body.DrawRotation);
var sa = MathF.Sin(-body.DrawRotation);
offset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y);
}
decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(drawPos.X + offset.X, -(drawPos.Y + offset.Y)), decorativeSpriteColor, origin,
-rotation + spriteRotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffects,
-rotation + spriteRotation, decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffects,
depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth));
}
}
@@ -795,6 +792,11 @@ namespace Barotrauma
}
}
}
foreach (var containedItem in ContainedItems)
{
containedItem.UpdateSpriteStates(deltaTime);
}
}
public override void UpdateEditing(Camera cam, float deltaTime)
@@ -1069,12 +1071,18 @@ namespace Barotrauma
foreach (RelatedItem relatedItem in requiredItems)
{
//TODO: add to localization
var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)),
relatedItem.Type.ToString() + " required", font: GUIStyle.SmallFont)
TextManager.Get($"{relatedItem.Type}.required").Fallback($"{relatedItem.Type} required"), font: GUIStyle.SmallFont)
{
Padding = new Vector4(10.0f, 0.0f, 10.0f, 0.0f)
};
var tooltip = TextManager.Get($"{relatedItem.Type}.required.tooltip").Fallback(LocalizedString.EmptyString);
if (!tooltip.IsNullOrWhiteSpace())
{
textBlock.ToolTip = tooltip;
}
textBlock.RectTransform.IsFixedSize = true;
componentEditor.AddCustomContent(textBlock, 1);
@@ -1543,10 +1551,19 @@ namespace Barotrauma
debugInitialHudPositions.Clear();
foreach (ItemComponent ic in activeHUDs)
{
if (ic.GuiFrame == null || ic.AllowUIOverlap || ic.GetLinkUIToComponent() != null) { continue; }
if (!ignoreLocking && ic.LockGuiFramePosition) { continue; }
//if the frame covers nearly all of the screen, don't trying to prevent overlaps because it'd fail anyway
if (ic.GuiFrame.Rect.Width >= GameMain.GraphicsWidth * 0.9f && ic.GuiFrame.Rect.Height >= GameMain.GraphicsHeight * 0.9f) { continue; }
if (ic.GuiFrame == null || ic.GetLinkUIToComponent() != null) { continue; }
bool nearlyCoversScreen = ic.GuiFrame.Rect.Width >= GameMain.GraphicsWidth * 0.9f &&
ic.GuiFrame.Rect.Height >= GameMain.GraphicsHeight * 0.9f;
// when we are not using overlap prevention, we still need to clamp the frame to the screen area to
// prevent frames becoming inaccessible outside the screen for example after a resolution change
if (ic.AllowUIOverlap || (!ignoreLocking && ic.LockGuiFramePosition) || nearlyCoversScreen)
{
ic.GuiFrame.ClampToArea(new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight));
continue;
}
ic.GuiFrame.RectTransform.ScreenSpaceOffset = ic.GuiFrameOffset;
elementsToMove.Add(ic.GuiFrame);
debugInitialHudPositions.Add(ic.GuiFrame.Rect);
@@ -1744,7 +1761,7 @@ namespace Barotrauma
if (texts.Any() && !recreateHudTexts) { return texts; }
texts.Clear();
string nameText = Name;
string nameText = RichString.Rich(Prefab.Name).SanitizedValue;
if (Prefab.Tags.Contains("identitycard") || Tags.Contains("despawncontainer"))
{
string[] readTags = Tags.Split(',');
@@ -2074,6 +2091,17 @@ namespace Barotrauma
}
}
break;
case EventType.SwapItem:
ushort newId = msg.ReadUInt16();
uint prefabUintId = msg.ReadUInt32();
ItemPrefab newPrefab = ItemPrefab.Prefabs.FirstOrDefault(p => p.UintIdentifier == prefabUintId);
if (newPrefab is null)
{
DebugConsole.ThrowError($"Error while reading {EventType.SwapItem} message: could not find an item prefab with the hash {prefabUintId}.");
break;
}
ReplaceFromNetwork(newPrefab, newId);
break;
default:
throw new Exception($"Malformed incoming item event: unsupported event type {eventType}");
}
@@ -2171,6 +2199,18 @@ namespace Barotrauma
}
}
//if the item is outside the level, but not in a sub, it implies the item is inside a sub server-side but the client failed to properly move it
// -> let's correct that by finding the correct sub
if (Level.IsPositionAboveLevel(WorldPosition) && Submarine == null)
{
var newSub = Submarine.FindContainingInLocalCoordinates(ConvertUnits.ToDisplayUnits(body.SimPosition), inflate: 0.0f);
if (newSub != null)
{
Submarine = newSub;
FindHull();
}
}
Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition);
rect.X = (int)(displayPos.X - rect.Width / 2.0f);
rect.Y = (int)(displayPos.Y + rect.Height / 2.0f);
@@ -2249,7 +2289,13 @@ namespace Barotrauma
if (!components.Contains(ic)) { return; }
var eventData = new ComponentStateEventData(ic, extraData);
if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false"); }
if (!ic.ValidateEventData(eventData)) {
string errorMsg =
$"Client-side component event creation for the item \"{Prefab.Identifier}\" failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false. " +
$"Data: {extraData?.GetType().ToString() ?? "null"}";
GameAnalyticsManager.AddErrorEventOnce($"Item.CreateClientEvent:ValidateEventData:{Prefab.Identifier}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg);
}
GameMain.Client.CreateEntityEvent(this, eventData);
}
@@ -2329,15 +2375,15 @@ namespace Barotrauma
ownerSheetIndex = (x, y);
}
bool tagsChanged = msg.ReadBoolean();
bool tagsChanged = msg.ReadBoolean();
string tags = "";
if (tagsChanged)
{
HashSet<Identifier> addedTags = msg.ReadString().Split(',').ToIdentifiers().ToHashSet();
HashSet<Identifier> removedTags = msg.ReadString().Split(',').ToIdentifiers().ToHashSet();
HashSet<Identifier> addedTags = msg.ReadString().ToIdentifiers().ToHashSet();
HashSet<Identifier> removedTags = msg.ReadString().ToIdentifiers().ToHashSet();
if (itemPrefab != null)
{
tags = string.Join(',',itemPrefab.Tags.Where(t => !removedTags.Contains(t)).Concat(addedTags));
tags = string.Join(',', itemPrefab.Tags.Where(t => !removedTags.Contains(t)).Union(addedTags));
}
}
@@ -2455,12 +2501,24 @@ namespace Barotrauma
if (inventory != null)
{
if (inventorySlotIndex >= 0 && inventorySlotIndex < 255 &&
inventory.TryPutItem(item, inventorySlotIndex, false, false, null, false))
if (inventorySlotIndex is >= 0 and < 255 &&
!inventory.TryPutItem(item, inventorySlotIndex, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false, ignoreCondition: true) &&
inventory.IsSlotEmpty(inventorySlotIndex))
{
return item;
//If the item won't go nicely, force it to the slot. If the server says the item is in the slot, it should go in the slot.
//May happen e.g. when a character is configured to spawn with an item that won't normally go in its inventory slots.
inventory.ForceToSlot(item, index: inventorySlotIndex);
}
else
{
inventory.TryPutItem(item, user: null, allowedSlots: item.AllowedSlots, createNetworkEvent: false);
}
item.SetTransform(inventory.Owner.SimPosition, 0.0f);
item.Submarine = inventory.Owner.Submarine;
if (inventory.Owner is Character { Enabled: false } && item.body != null)
{
item.body.Enabled = false;
}
inventory.TryPutItem(item, null, item.AllowedSlots, false);
}
return item;

View File

@@ -20,7 +20,8 @@ namespace Barotrauma
public override bool IsVisible(Rectangle worldView)
{
return Screen.Selected == GameMain.SubEditorScreen || GameMain.DebugDraw;
if (Screen.Selected != GameMain.SubEditorScreen && !GameMain.DebugDraw) { return false; }
return base.IsVisible(worldView);
}
public override void Draw(SpriteBatch sb, bool editing, bool back = true)
@@ -95,9 +96,13 @@ namespace Barotrauma
new Vector2(Math.Sign(targetHull.Rect.Center.X - rect.Center.X), 0.0f)
: new Vector2(0.0f, Math.Sign((rect.Y - rect.Height / 2.0f) - (targetHull.Rect.Y - targetHull.Rect.Height / 2.0f)));
Vector2 arrowPos = new Vector2(WorldRect.Center.X, -(WorldRect.Y - WorldRect.Height / 2));
Vector2 arrowPos = new Vector2(WorldRect.Center.X, WorldRect.Y - WorldRect.Height / 2);
if (Submarine != null)
{
arrowPos += (Submarine.DrawPosition - Submarine.Position);
}
arrowPos.Y = -arrowPos.Y;
arrowPos += new Vector2(dir.X * (WorldRect.Width / 2), dir.Y * (WorldRect.Height / 2));
bool invalidDir = false;
if (dir == Vector2.Zero)
{

View File

@@ -81,16 +81,12 @@ namespace Barotrauma
public override bool IsVisible(Rectangle worldView)
{
if (BallastFlora != null) { return true; }
if (Screen.Selected != GameMain.SubEditorScreen && !GameMain.DebugDraw)
{
if (decals.Count == 0 && paintAmount < minimumPaintAmountToDraw) { return false; }
Rectangle worldRect = WorldRect;
if (worldRect.X > worldView.Right || worldRect.Right < worldView.X) { return false; }
if (worldRect.Y < worldView.Y - worldView.Height || worldRect.Y - worldRect.Height > worldView.Y) { return false; }
}
return true;
return base.IsVisible(worldView);
}
public override bool IsMouseOn(Vector2 position)
@@ -103,12 +99,31 @@ namespace Barotrauma
private GUIComponent CreateEditingHUD(bool inGame = false)
{
int heightScaled = GUI.IntScale(20);
editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this };
GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null)
{
CanTakeKeyBoardFocus = false
};
new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont);
var hullEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont);
if (!inGame)
{
if (Linkable)
{
var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("HoldToLink"), font: GUIStyle.SmallFont);
var hullLinkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("hulllinkinfo"), font: GUIStyle.SmallFont);
var itemsText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("AllowedLinks"), font: GUIStyle.SmallFont);
LocalizedString allowedItems = AllowedLinks.None() ? TextManager.Get("None") : string.Join(", ", AllowedLinks);
itemsText.Text = TextManager.AddPunctuation(':', itemsText.Text, allowedItems);
hullEditor.AddCustomContent(linkText, 1);
hullEditor.AddCustomContent(hullLinkText, 2);
hullEditor.AddCustomContent(itemsText, 3);
linkText.TextColor = GUIStyle.Orange;
hullLinkText.TextColor = GUIStyle.Orange;
itemsText.TextColor = GUIStyle.Orange;
}
}
PositionEditingHUD();
@@ -389,8 +404,8 @@ namespace Barotrauma
//GUI.DrawRectangle(spriteBatch, new Rectangle((int)fs.LastExtinguishPos.X, (int)-fs.LastExtinguishPos.Y, 5,5), Color.Yellow, true);
}
GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f);
float worldSurface = surface + Submarine.DrawPosition.Y;
GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -worldSurface), new Vector2(drawRect.Right, -worldSurface), Color.Cyan * 0.5f);
for (int i = 0; i < waveY.Length - 1; i++)
{
GUI.DrawLine(spriteBatch,
@@ -564,7 +579,6 @@ namespace Barotrauma
//bottom right
corners[5] = new Vector3(x + width, bottom, 0.0f);
Vector2[] uvCoords = new Vector2[4];
for (int n = 0; n < 4; n++)
{
uvCoords[n] = Vector2.Transform(new Vector2(corners[n].X, -corners[n].Y), transform);

View File

@@ -26,7 +26,7 @@ namespace Barotrauma
private Vector3 velocity;
private float depth;
public float Depth { get; private set; }
private float alpha = 1.0f;
@@ -42,6 +42,8 @@ namespace Barotrauma
Vector2 drawPosition;
private bool flippedHorizontally;
public Vector2[,] CurrentSpriteDeformation
{
get;
@@ -88,6 +90,8 @@ namespace Barotrauma
Rand.Range(-prefab.Speed, prefab.Speed, Rand.RandSync.ClientOnly),
Rand.Range(0.0f, prefab.WanderZAmount, Rand.RandSync.ClientOnly));
Depth = Rand.Range(prefab.MinDepth, prefab.MaxDepth, Rand.RandSync.ClientOnly);
checkWallsTimer = Rand.Range(0.0f, CheckWallsInterval, Rand.RandSync.ClientOnly);
foreach (var subElement in prefab.Config.Elements())
@@ -104,6 +108,7 @@ namespace Barotrauma
default:
continue;
}
int j = 0;
foreach (XElement animationElement in subElement.Elements())
{
SpriteDeformation deformation = null;
@@ -118,7 +123,21 @@ namespace Barotrauma
deformation = SpriteDeformation.Load(animationElement, prefab.Name);
if (deformation != null)
{
deformation.Params = Prefab.SpriteDeformations[j].Params;
uniqueSpriteDeformations.Add(deformation);
if (prefab.DeformableSprite != null)
{
if (deformation.Resolution.X > prefab.DeformableSprite.Subdivisions.X ||
deformation.Resolution.Y > prefab.DeformableSprite.Subdivisions.Y)
{
DebugConsole.AddWarning(
$"Potential error in background creature {Prefab.Identifier}: deformation {deformation.GetType()} has a larger resolution ({deformation.Resolution})"+
$" than the amount of subdivisions on the deformable sprite ({prefab.DeformableSprite.Subdivisions}). Should the sprite be subdivided further to make full use of the deformation?",
contentPackage: Prefab.ContentPackage);
}
}
j++;
}
}
if (deformation != null)
@@ -127,12 +146,14 @@ namespace Barotrauma
}
}
}
flashTimer = Rand.Range(0.0f, prefab.FlashInterval, Rand.RandSync.Unsynced);
}
public void Update(float deltaTime)
{
position += new Vector2(velocity.X, velocity.Y) * deltaTime;
depth = MathHelper.Clamp(depth + velocity.Z * deltaTime, Prefab.MinDepth, Prefab.MaxDepth * 10);
Depth = MathHelper.Clamp(Depth + velocity.Z * deltaTime, Prefab.MinDepth, Prefab.MaxDepth);
if (Prefab.FlashInterval > 0.0f)
{
@@ -144,7 +165,7 @@ namespace Barotrauma
else
{
//value goes from 0 to 1 and back to 0 during the flash
alpha = (float)Math.Sin(-flashTimer / Prefab.FlashDuration * MathHelper.Pi) * PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.1f, (float)Timing.TotalTime * 0.2f);
alpha = (float)Math.Sin(-flashTimer / Prefab.FlashDuration * MathHelper.Pi) * PerlinNoise.GetPerlin((float)Timing.TotalTime, (float)Timing.TotalTime * 0.5f);
if (flashTimer < -Prefab.FlashDuration)
{
flashTimer = Prefab.FlashInterval;
@@ -228,7 +249,13 @@ namespace Barotrauma
velocity = Vector3.Lerp(velocity, new Vector3(Steering.X, Steering.Y, velocity.Z), deltaTime);
UpdateDeformations(deltaTime);
//only flip if there's some horizontal movement speed (10% of the creature's speed)
//otherwise a creature swimming roughly up/down can flip around very frequently when the horizontal speed fluctuates around 0
if (Math.Abs(velocity.X) > Prefab.Speed * 0.1f)
{
flippedHorizontally = !Prefab.DisableFlipping && velocity.X < 0.0f;
}
UpdateDeformations(deltaTime, flippedHorizontally);
}
public void DrawLightSprite(SpriteBatch spriteBatch, Camera cam)
@@ -238,12 +265,17 @@ namespace Barotrauma
public void Draw(SpriteBatch spriteBatch, Camera cam)
{
Color color =
Prefab.FadeOut ?
Color.Lerp(Color.White, Level.Loaded.BackgroundColor, Depth / Prefab.FadeOutDepth) * alpha :
Color.White * alpha;
Draw(spriteBatch,
cam,
Prefab.Sprite,
Prefab.DeformableSprite,
CurrentSpriteDeformation,
Color.Lerp(Color.White, Level.Loaded.BackgroundColor, depth / Math.Max(MaxDepth, Prefab.MaxDepth)) * alpha);
color);
}
private void Draw(SpriteBatch spriteBatch, Camera cam, Sprite sprite, DeformableSprite deformableSprite, Vector2[,] currentSpriteDeformation, Color color)
@@ -255,7 +287,7 @@ namespace Barotrauma
if (!Prefab.DisableRotation)
{
rotation = MathUtils.VectorToAngle(new Vector2(velocity.X, -velocity.Y));
if (velocity.X < 0.0f) { rotation -= MathHelper.Pi; }
if (flippedHorizontally) { rotation -= MathHelper.Pi; }
}
drawPosition = GetDrawPosition(cam);
@@ -266,8 +298,8 @@ namespace Barotrauma
color,
rotation,
scale,
Prefab.DisableFlipping || velocity.X > 0.0f ? SpriteEffects.None : SpriteEffects.FlipHorizontally,
Math.Min(depth / MaxDepth, 1.0f));
flippedHorizontally ? SpriteEffects.FlipHorizontally : SpriteEffects.None,
Math.Min(Depth / MaxDepth, 1.0f));
if (deformableSprite != null)
{
@@ -280,29 +312,29 @@ namespace Barotrauma
deformableSprite.Reset();
}
deformableSprite?.Draw(cam,
new Vector3(drawPosition.X, drawPosition.Y, Math.Min(depth / 10000.0f, 1.0f)),
new Vector3(drawPosition.X, drawPosition.Y, Math.Min(Depth / 10000.0f, 1.0f)),
deformableSprite.Origin,
rotation,
Vector2.One * scale,
color,
mirror: Prefab.DisableFlipping || velocity.X <= 0.0f);
mirror: flippedHorizontally);
}
}
public Vector2 GetDrawPosition(Camera cam)
{
Vector2 drawPosition = WorldPosition;
if (depth >= 0)
if (Depth >= 0)
{
Vector2 camOffset = drawPosition - cam.WorldViewCenter;
drawPosition -= camOffset * depth / MaxDepth;
drawPosition -= camOffset * Depth / MaxDepth;
}
return drawPosition;
}
public float GetScale()
{
return Math.Max(1.0f - depth / MaxDepth, 0.05f) * Prefab.Scale;
return Math.Max(1.0f - Depth / MaxDepth, 0.05f) * Prefab.Scale;
}
public Rectangle GetExtents(Camera cam)
@@ -328,7 +360,7 @@ namespace Barotrauma
}
}
private void UpdateDeformations(float deltaTime)
private void UpdateDeformations(float deltaTime, bool flippedHorizontally)
{
foreach (SpriteDeformation deformation in uniqueSpriteDeformations)
{
@@ -336,11 +368,13 @@ namespace Barotrauma
}
if (spriteDeformations.Count > 0)
{
CurrentSpriteDeformation = SpriteDeformation.GetDeformation(spriteDeformations, Prefab.DeformableSprite.Size);
CurrentSpriteDeformation = SpriteDeformation.GetDeformation(spriteDeformations, Prefab.DeformableSprite.Size,
flippedHorizontally: flippedHorizontally);
}
if (lightSpriteDeformations.Count > 0)
{
CurrentLightSpriteDeformation = SpriteDeformation.GetDeformation(lightSpriteDeformations, Prefab.DeformableLightSprite.Size);
CurrentLightSpriteDeformation = SpriteDeformation.GetDeformation(lightSpriteDeformations, Prefab.DeformableLightSprite.Size,
flippedHorizontally: flippedHorizontally);
}
}
}

View File

@@ -15,18 +15,19 @@ namespace Barotrauma
private float checkVisibleTimer;
private readonly List<BackgroundCreaturePrefab> prefabs = new List<BackgroundCreaturePrefab>();
private readonly List<BackgroundCreature> creatures = new List<BackgroundCreature>();
public BackgroundCreatureManager(IEnumerable<BackgroundCreaturePrefabsFile> files)
private readonly List<BackgroundCreature> visibleCreatures = new List<BackgroundCreature>();
public BackgroundCreatureManager()
{
foreach(var file in files)
/*foreach(var file in files)
{
LoadConfig(file.Path);
}
}*/
}
public BackgroundCreatureManager(string path)
/*public BackgroundCreatureManager(string path)
{
DebugConsole.AddWarning($"Couldn't find any BackgroundCreaturePrefabs files, falling back to {path}");
LoadConfig(ContentPath.FromRaw(null, path));
@@ -42,35 +43,34 @@ namespace Barotrauma
if (mainElement.IsOverride())
{
mainElement = mainElement.FirstElement();
prefabs.Clear();
Prefabs.Clear();
DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.MediumPurple);
}
else if (prefabs.Any())
else if (Prefabs.Any())
{
DebugConsole.NewMessage($"Loading additional background creatures from file '{configPath}'");
}
foreach (var element in mainElement.Elements())
{
prefabs.Add(new BackgroundCreaturePrefab(element));
Prefabs.Add(new BackgroundCreaturePrefab(element));
};
}
catch (Exception e)
{
DebugConsole.ThrowError(String.Format("Failed to load BackgroundCreatures from {0}", configPath), e);
}
}
}*/
public void SpawnCreatures(Level level, int count, Vector2? position = null)
{
creatures.Clear();
if (prefabs.Count == 0) { return; }
List<BackgroundCreaturePrefab> availablePrefabs = new List<BackgroundCreaturePrefab>(BackgroundCreaturePrefab.Prefabs.OrderBy(p => p.Identifier.Value));
if (availablePrefabs.Count == 0) { return; }
count = Math.Min(count, MaxCreatures);
List<BackgroundCreaturePrefab> availablePrefabs = new List<BackgroundCreaturePrefab>(prefabs);
for (int i = 0; i < count; i++)
{
Vector2 pos = Vector2.Zero;
@@ -93,7 +93,7 @@ namespace Barotrauma
pos = (Vector2)position;
}
var prefab = ToolBox.SelectWeightedRandom(availablePrefabs, availablePrefabs.Select(p => p.GetCommonness(level.GenerationParams)).ToList(), Rand.RandSync.ClientOnly);
var prefab = ToolBox.SelectWeightedRandom(availablePrefabs, availablePrefabs.Select(p => p.GetCommonness(level?.LevelData)).ToList(), Rand.RandSync.ClientOnly);
if (prefab == null) { break; }
int amount = Rand.Range(prefab.SwarmMin, prefab.SwarmMax + 1, Rand.RandSync.ClientOnly);
@@ -125,16 +125,27 @@ namespace Barotrauma
{
if (checkVisibleTimer < 0.0f)
{
visibleCreatures.Clear();
int margin = 500;
foreach (BackgroundCreature creature in creatures)
{
Rectangle extents = creature.GetExtents(cam);
bool wasVisible = creature.Visible;
creature.Visible =
extents.Right >= cam.WorldView.X - margin &&
extents.X <= cam.WorldView.Right + margin &&
extents.Bottom >= cam.WorldView.Y - cam.WorldView.Height - margin &&
extents.Y <= cam.WorldView.Y + margin;
if (creature.Visible)
{
//insertion sort according to depth
int i = 0;
while (i < visibleCreatures.Count)
{
if (visibleCreatures[i].Depth < creature.Depth) { break; }
i++;
}
visibleCreatures.Insert(i, creature);
}
}
checkVisibleTimer = VisibilityCheckInterval;
@@ -144,27 +155,24 @@ namespace Barotrauma
checkVisibleTimer -= deltaTime;
}
foreach (BackgroundCreature creature in creatures)
foreach (BackgroundCreature creature in visibleCreatures)
{
if (!creature.Visible) { continue; }
creature.Update(deltaTime);
}
}
public void Draw(SpriteBatch spriteBatch, Camera cam)
{
foreach (BackgroundCreature creature in creatures)
foreach (BackgroundCreature creature in visibleCreatures)
{
if (!creature.Visible) { continue; }
creature.Draw(spriteBatch, cam);
}
}
public void DrawLights(SpriteBatch spriteBatch, Camera cam)
{
foreach (BackgroundCreature creature in creatures)
foreach (BackgroundCreature creature in visibleCreatures)
{
if (!creature.Visible) { continue; }
creature.DrawLightSprite(spriteBatch, cam);
}
}

View File

@@ -1,65 +1,91 @@
using System.Collections.Generic;
using Barotrauma.SpriteDeformations;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
class BackgroundCreaturePrefab
class BackgroundCreaturePrefab : Prefab, ISerializableEntity
{
public readonly Sprite Sprite, LightSprite;
public readonly DeformableSprite DeformableSprite, DeformableLightSprite;
public readonly static PrefabCollection<BackgroundCreaturePrefab> Prefabs = new PrefabCollection<BackgroundCreaturePrefab>();
public readonly string Name;
public Sprite Sprite { get; private set; }
public Sprite LightSprite { get; private set; }
public DeformableSprite DeformableSprite { get; private set; }
public DeformableSprite DeformableLightSprite { get; private set; }
private readonly string name;
public readonly XElement Config;
[Serialize(1.0f, IsPropertySaveable.Yes)]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float Speed { get; private set; }
[Serialize(0.0f, IsPropertySaveable.Yes)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 3)]
public float WanderAmount { get; private set; }
[Serialize(0.0f, IsPropertySaveable.Yes)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 3)]
public float WanderZAmount { get; private set; }
[Serialize(1, IsPropertySaveable.Yes)]
[Serialize(1, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000)]
public int SwarmMin { get; private set; }
[Serialize(1, IsPropertySaveable.Yes)]
[Serialize(1, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000)]
public int SwarmMax { get; private set; }
[Serialize(200.0f, IsPropertySaveable.Yes)]
[Serialize(200.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float SwarmRadius { get; private set; }
[Serialize(0.2f, IsPropertySaveable.Yes)]
[Serialize(0.2f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float SwarmCohesion { get; private set; }
[Serialize(10.0f, IsPropertySaveable.Yes)]
[Serialize(10.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float MinDepth { get; private set; }
[Serialize(1000.0f, IsPropertySaveable.Yes)]
[Serialize(1000.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float MaxDepth { get; private set; }
[Serialize(false, IsPropertySaveable.Yes)]
[Serialize(10000.0f, IsPropertySaveable.Yes, description: "Creatures fade out to the background color of the level the further they are from the camera. This value is the depth at which the object becomes \"maximally\" faded out."), Editable]
public float FadeOutDepth
{
get;
private set;
}
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool FadeOut { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool DisableRotation { get; private set; }
[Serialize(false, IsPropertySaveable.Yes)]
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool DisableFlipping { get; private set; }
[Serialize(1.0f, IsPropertySaveable.Yes)]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float Scale { get; private set; }
[Serialize(1.0f, IsPropertySaveable.Yes)]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float Commonness { get; private set; }
[Serialize(1000, IsPropertySaveable.Yes)]
[Serialize(1000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000)]
public int MaxCount { get; private set; }
[Serialize(0.0f, IsPropertySaveable.Yes)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float FlashInterval { get; private set; }
[Serialize(0.0f, IsPropertySaveable.Yes)]
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float FlashDuration { get; private set; }
public string Name => name;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
/// <summary>
/// Only used for editing sprite deformation parameters. The actual LevelObjects use separate SpriteDeformation instances.
/// </summary>
public List<SpriteDeformation> SpriteDeformations
{
get;
private set;
} = new List<SpriteDeformation>();
/// <summary>
/// Overrides the commonness of the object in a specific level type.
@@ -67,13 +93,13 @@ namespace Barotrauma
/// </summary>
public Dictionary<Identifier, float> OverrideCommonness = new Dictionary<Identifier, float>();
public BackgroundCreaturePrefab(ContentXElement element)
public BackgroundCreaturePrefab(ContentXElement element, BackgroundCreaturePrefabsFile file) : base(file, ParseIdentifier(element))
{
Name = element.Name.ToString();
name = element.Name.ToString();
Config = element;
SerializableProperty.DeserializeProperties(this, element);
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
foreach (var subElement in element.Elements())
{
@@ -84,6 +110,14 @@ namespace Barotrauma
break;
case "deformablesprite":
DeformableSprite = new DeformableSprite(subElement, lazyLoad: true);
foreach (XElement deformElement in subElement.Elements())
{
var deformation = SpriteDeformation.Load(deformElement, Name);
if (deformation != null)
{
SpriteDeformations.Add(deformation);
}
}
break;
case "lightsprite":
LightSprite = new Sprite(subElement, lazyLoad: true);
@@ -102,17 +136,42 @@ namespace Barotrauma
}
}
public float GetCommonness(LevelGenerationParams generationParams)
public static Identifier ParseIdentifier(XElement element)
{
if (generationParams != null &&
!generationParams.Identifier.IsEmpty &&
(OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) ||
(!generationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness))))
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
if (identifier.IsEmpty)
{
identifier = element.NameAsIdentifier();
}
return identifier;
}
public float GetCommonness(LevelData levelData)
{
if (levelData?.GenerationParams is not { } generationParams || generationParams.Identifier.IsEmpty)
{
return Commonness;
}
if (OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) || (!generationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)) ||
OverrideCommonness.TryGetValue(levelData.Biome.Identifier, out commonness))
{
return commonness;
}
return Commonness;
}
public override void Dispose()
{
Sprite?.Remove();
Sprite = null;
LightSprite?.Remove();
LightSprite = null;
DeformableLightSprite?.Remove();
DeformableLightSprite = null;
DeformableSprite?.Remove();
DeformableSprite = null;
}
}
}

View File

@@ -9,26 +9,56 @@ namespace Barotrauma
{
static partial class CaveGenerator
{
public static List<VertexPositionTexture> GenerateWallVertices(List<Vector2[]> triangles, LevelGenerationParams generationParams, float zCoord)
public static List<VertexPositionColor> GenerateWallVertices(List<Vector2[]> triangles, Color color, float zCoord)
{
var vertices = new List<VertexPositionTexture>();
var vertices = new List<VertexPositionColor>();
for (int i = 0; i < triangles.Count; i++)
{
foreach (Vector2 vertex in triangles[i])
{
Vector2 uvCoords = vertex / generationParams.WallTextureSize;
vertices.Add(new VertexPositionTexture(new Vector3(vertex, zCoord), uvCoords));
vertices.Add(new VertexPositionColor(new Vector3(vertex, zCoord), color));
}
}
return vertices;
}
public static List<VertexPositionTexture> GenerateWallEdgeVertices(List<VoronoiCell> cells, Level level, float zCoord)
/// <summary>
/// Generates texture coordinates for the vertices based on their positions
/// </summary>
public static VertexPositionColorTexture[] ConvertToTextured(VertexPositionColor[] verts, float textureSize)
{
float outWardThickness = level.GenerationParams.WallEdgeExpandOutwardsAmount;
VertexPositionColorTexture[] texturedVerts = new VertexPositionColorTexture[verts.Length];
for (int i = 0; i < verts.Length; i++)
{
VertexPositionColor vertex = verts[i];
texturedVerts[i] = new VertexPositionColorTexture(vertex.Position, vertex.Color, textureCoordinate: Vector2.Zero);
}
GenerateTextureCoordinates(texturedVerts, textureSize);
return texturedVerts;
}
List<VertexPositionTexture> vertices = new List<VertexPositionTexture>();
/// <summary>
/// Generates texture coordinates for the vertices based on their positions
/// </summary>
public static void GenerateTextureCoordinates(VertexPositionColorTexture[] verts, float textureSize)
{
for (int i = 0; i < verts.Length; i++)
{
VertexPositionColorTexture vertex = verts[i];
Vector2 uvCoords = new Vector2(vertex.Position.X, vertex.Position.Y) / textureSize;
verts[i] = new VertexPositionColorTexture(verts[i].Position, verts[i].Color, uvCoords);
}
}
public static List<VertexPositionColorTexture> GenerateWallEdgeVertices(
List<VoronoiCell> cells,
float expandOutwards, float expandInwards,
Color outerColor, Color innerColor,
Level level, float zCoord, bool preventExpandThroughCell = false)
{
float outWardThickness = expandOutwards;
List<VertexPositionColorTexture> vertices = new List<VertexPositionColorTexture>();
foreach (VoronoiCell cell in cells)
{
Vector2 minVert = cell.Edges[0].Point1;
@@ -49,7 +79,10 @@ namespace Barotrauma
{
if (!edge.IsSolid) { continue; }
GraphEdge leftEdge = cell.Edges.Find(e => e != edge && (edge.Point1.NearlyEquals(e.Point1) || edge.Point1.NearlyEquals(e.Point2)));
//the left-side edge on this same cell
GraphEdge myLeftEdge = cell.Edges.Find(e => e != edge && (edge.Point1.NearlyEquals(e.Point1) || edge.Point1.NearlyEquals(e.Point2)));
//the left-side edge on either this cell, or the adjacent one if this is attached to another cell
GraphEdge leftEdge = myLeftEdge;
var leftAdjacentCell = leftEdge?.AdjacentCell(cell);
if (leftAdjacentCell != null)
{
@@ -57,7 +90,10 @@ namespace Barotrauma
if (adjEdge != null) { leftEdge = adjEdge; }
}
GraphEdge rightEdge = cell.Edges.Find(e => e != edge && (edge.Point2.NearlyEquals(e.Point1) || edge.Point2.NearlyEquals(e.Point2)));
//the right-side edge on this same cell
GraphEdge myRightEdge = cell.Edges.Find(e => e != edge && (edge.Point2.NearlyEquals(e.Point1) || edge.Point2.NearlyEquals(e.Point2)));
//the right-side edge on either this cell, or the adjacent one if this is attached to another cell
GraphEdge rightEdge = myRightEdge;
var rightAdjacentCell = rightEdge?.AdjacentCell(cell);
if (rightAdjacentCell != null)
{
@@ -67,18 +103,25 @@ namespace Barotrauma
Vector2 leftNormal = Vector2.Zero, rightNormal = Vector2.Zero;
float inwardThickness1 = level.GenerationParams.WallEdgeExpandInwardsAmount;
float inwardThickness2 = level.GenerationParams.WallEdgeExpandInwardsAmount;
float inwardThickness1 = Math.Min(expandInwards, edge.Length);
float inwardThickness2 = inwardThickness1;
if (leftEdge != null && !leftEdge.IsSolid)
{
//the left-side edge is non-solid (an edge between two cells, not an actual solid wall edge)
// -> expand in the direction of that edge
leftNormal = edge.Point1.NearlyEquals(leftEdge.Point1) ?
Vector2.Normalize(leftEdge.Point2 - leftEdge.Point1) :
Vector2.Normalize(leftEdge.Point1 - leftEdge.Point2);
//maximum expansion is half of the size of the edge (otherwise the expansions from different sides of the edge could overlap or even extend "through" the cell)
inwardThickness1 = Math.Min(inwardThickness1, leftEdge.Length / 2);
}
else if (leftEdge != null)
{
//use the average of this edge's and the adjacent edge's normals
leftNormal = -Vector2.Normalize(edge.GetNormal(cell) + leftEdge.GetNormal(leftAdjacentCell ?? cell));
if (!MathUtils.IsValid(leftNormal)) { leftNormal = -edge.GetNormal(cell); }
//maximum expansion is the length of the adjacent edge (more expansion causes the textures to distort)
inwardThickness1 = Math.Min(Math.Min(inwardThickness1, leftEdge.Length), myLeftEdge.Length);
}
else
{
@@ -109,11 +152,13 @@ namespace Barotrauma
rightNormal = edge.Point2.NearlyEquals(rightEdge.Point1) ?
Vector2.Normalize(rightEdge.Point2 - rightEdge.Point1) :
Vector2.Normalize(rightEdge.Point1 - rightEdge.Point2);
inwardThickness2 = Math.Min(inwardThickness2, rightEdge.Length / 2);
}
else if (rightEdge != null)
{
rightNormal = -Vector2.Normalize(edge.GetNormal(cell) + rightEdge.GetNormal(rightAdjacentCell ?? cell));
if (!MathUtils.IsValid(rightNormal)) { rightNormal = -edge.GetNormal(cell); }
inwardThickness2 = Math.Min(Math.Min(inwardThickness2, rightEdge.Length), myRightEdge.Length);
}
else
{
@@ -150,10 +195,51 @@ namespace Barotrauma
point1UV = point1UV / MathHelper.TwoPi * textureRepeatCount;
point2UV = point2UV / MathHelper.TwoPi * textureRepeatCount;
//if calculating the UVs based on polar coordinates would result in stretching (using less than 10% of the texture for a wall the size of the texture)
//just calculate the UVs based on the length of the wall
//(this will mean the textures don't align at point2, but it doesn't seem that noticeable)
if ((point2UV - point1UV) * level.GenerationParams.WallEdgeTextureWidth < edge.Length * 0.1f)
{
point2UV = point1UV + edge.Length / 2 / level.GenerationParams.WallEdgeTextureWidth;
}
//"extruding" inwards, need to make sure we don't make the edge poke through the cell from the other side
if (preventExpandThroughCell)
{
foreach (GraphEdge otherEdge in cell.Edges)
{
if (otherEdge == edge || Vector2.Dot(otherEdge.GetNormal(cell), edge.GetNormal(cell)) > 0) { continue; }
if (otherEdge != leftEdge)
{
inwardThickness1 = ClampThickness(otherEdge, edge.Point1, leftNormal, inwardThickness1);
}
if (otherEdge != rightEdge)
{
inwardThickness2 = ClampThickness(otherEdge, edge.Point2, rightNormal, inwardThickness2);
}
}
static float ClampThickness(GraphEdge otherEdge, Vector2 thisPoint, Vector2 thisEdgeNormal, float currThickness)
{
if (MathUtils.GetLineIntersection(
thisPoint, thisPoint + thisEdgeNormal * currThickness,
otherEdge.Point1, otherEdge.Point2, areLinesInfinite: false, out Vector2 intersection1))
{
return Math.Min(currThickness, Vector2.Distance(thisPoint, intersection1));
}
return currThickness;
}
}
//there needs to be some minimum amount of inward thickness,
//if the edge texture doesn't extend inside at all you can see through between the edge texture and the solid part of the cell
inwardThickness1 = Math.Max(inwardThickness1, Math.Min(100.0f, expandInwards));
inwardThickness2 = Math.Max(inwardThickness2, Math.Min(100.0f, expandInwards));
for (int i = 0; i < 2; i++)
{
Vector2[] verts = new Vector2[3];
VertexPositionTexture[] vertPos = new VertexPositionTexture[3];
VertexPositionColorTexture[] vertPos = new VertexPositionColorTexture[3];
if (i == 0)
{
@@ -161,9 +247,9 @@ namespace Barotrauma
verts[1] = edge.Point2 - rightNormal * outWardThickness;
verts[2] = edge.Point1 + leftNormal * inwardThickness1;
vertPos[0] = new VertexPositionTexture(new Vector3(verts[0], zCoord), new Vector2(point1UV, 0.0f));
vertPos[1] = new VertexPositionTexture(new Vector3(verts[1], zCoord), new Vector2(point2UV, 0.0f));
vertPos[2] = new VertexPositionTexture(new Vector3(verts[2], zCoord), new Vector2(point1UV, 1.0f));
vertPos[0] = new VertexPositionColorTexture(new Vector3(verts[0], zCoord), outerColor, new Vector2(point1UV, 0.0f));
vertPos[1] = new VertexPositionColorTexture(new Vector3(verts[1], zCoord), outerColor, new Vector2(point2UV, 0.0f));
vertPos[2] = new VertexPositionColorTexture(new Vector3(verts[2], zCoord), innerColor, new Vector2(point1UV, 1.0f));
}
else
{
@@ -172,9 +258,9 @@ namespace Barotrauma
verts[1] = edge.Point2 - rightNormal * outWardThickness;
verts[2] = edge.Point2 + rightNormal * inwardThickness2;
vertPos[0] = new VertexPositionTexture(new Vector3(verts[0], zCoord), new Vector2(point1UV, 1.0f));
vertPos[1] = new VertexPositionTexture(new Vector3(verts[1], zCoord), new Vector2(point2UV, 0.0f));
vertPos[2] = new VertexPositionTexture(new Vector3(verts[2], zCoord), new Vector2(point2UV, 1.0f));
vertPos[0] = new VertexPositionColorTexture(new Vector3(verts[0], zCoord), innerColor, new Vector2(point1UV, 1.0f));
vertPos[1] = new VertexPositionColorTexture(new Vector3(verts[1], zCoord), outerColor, new Vector2(point2UV, 0.0f));
vertPos[2] = new VertexPositionColorTexture(new Vector3(verts[2], zCoord), innerColor, new Vector2(point2UV, 1.0f));
}
vertices.AddRange(vertPos);
}

View File

@@ -47,62 +47,75 @@ namespace Barotrauma
if (renderer == null) { return; }
renderer.DrawDebugOverlay(spriteBatch, cam);
if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f)
if (GameMain.DebugDraw)
{
foreach (InterestingPosition pos in PositionsOfInterest)
if (Screen.Selected.Cam.Zoom > 0.1f)
{
Color color = Color.Yellow;
if (pos.PositionType == PositionType.Cave || pos.PositionType == PositionType.AbyssCave)
foreach (InterestingPosition pos in PositionsOfInterest)
{
color = Color.DarkOrange;
}
else if (pos.PositionType == PositionType.Ruin)
{
color = Color.LightGray;
}
if (!pos.IsValid)
{
color = Color.Red;
Color color = Color.Yellow;
if (pos.PositionType == PositionType.Cave || pos.PositionType == PositionType.AbyssCave)
{
color = Color.DarkOrange;
}
else if (pos.PositionType == PositionType.Ruin)
{
color = Color.LightGray;
}
if (!pos.IsValid)
{
color = Color.Red;
}
GUI.DrawRectangle(spriteBatch, new Vector2(pos.Position.X - 15.0f, -pos.Position.Y - 15.0f), new Vector2(30.0f, 30.0f), color, true);
}
GUI.DrawRectangle(spriteBatch, new Vector2(pos.Position.X - 15.0f, -pos.Position.Y - 15.0f), new Vector2(30.0f, 30.0f), color, true);
foreach (RuinGeneration.Ruin ruin in Ruins)
{
Rectangle ruinArea = ruin.Area;
ruinArea.Y = -ruinArea.Y - ruinArea.Height;
GUI.DrawRectangle(spriteBatch, ruinArea, Color.DarkSlateBlue, false, 0, 5);
}
}
foreach (RuinGeneration.Ruin ruin in Ruins)
{
Rectangle ruinArea = ruin.Area;
ruinArea.Y = -ruinArea.Y - ruinArea.Height;
GUI.DrawRectangle(spriteBatch, ruinArea, Color.DarkSlateBlue, false, 0, 5);
}
foreach (var positions in wreckPositions.Values)
float zoomFactor = MathHelper.Lerp(20, 1, MathUtils.InverseLerp(Screen.Selected.Cam.MinZoom, Screen.Selected.Cam.DefaultZoom, Screen.Selected.Cam.Zoom));
foreach ((string debugInfo, List<Vector2> positions) in positionHistory)
{
for (int i = 0; i < positions.Count; i++)
{
float t = (i + 1) / (float)positions.Count;
float multiplier = MathHelper.Lerp(0, 1, t);
float multiplier = MathHelper.Lerp(0.1f, 1, t);
Color color = Color.Red * multiplier;
var pos = positions[i];
pos.Y = -pos.Y;
var size = new Vector2(100);
GUI.DrawRectangle(spriteBatch, pos - size / 2, size, color, thickness: 10);
var size = new Vector2(200);
if (i == 0)
{
GUI.DrawRectangle(spriteBatch, pos - size, size * 2, Color.Red, thickness: 2 * zoomFactor);
GUI.DrawString(spriteBatch, pos - new Vector2(10, 20), debugInfo, Color.White, font: GUIStyle.LargeFont, forceUpperCase: ForceUpperCase.Yes);
}
if (i < positions.Count - 1)
{
if (i > 0)
{
GUI.DrawRectangle(spriteBatch, pos - size / 2, size, Color.Red, isFilled: true);
}
var nextPos = positions[i + 1];
nextPos.Y = -nextPos.Y;
GUI.DrawLine(spriteBatch, pos, nextPos, color, width: 10);
GUI.DrawLine(spriteBatch, pos, nextPos, color, width: 4 * zoomFactor);
}
}
}
foreach (var rects in blockedRects.Values)
foreach ((Submarine sub, List<Rectangle> rects) in blockedRects)
{
foreach (var rect in rects)
foreach (Rectangle t in rects)
{
Rectangle newRect = rect;
Rectangle newRect = t;
newRect.Y = -newRect.Y;
GUI.DrawRectangle(spriteBatch, newRect, Color.Red, thickness: 5);
GUI.DrawRectangle(spriteBatch, newRect, Color.Red * 0.1f, isFilled: true);
GUI.DrawString(spriteBatch, newRect.Center.ToVector2(), $"{sub.Info.Name}", Color.White, font: GUIStyle.LargeFont, forceUpperCase: ForceUpperCase.Yes);
}
}
}

View File

@@ -167,7 +167,7 @@ namespace Barotrauma
Prefab.OverrideProperties.Any(p => p != null && (p.Sprites.Any() || p.DeformableSprite != null));
}
public void Update(float deltaTime)
public void Update(float deltaTime, Camera cam)
{
CurrentRotation = Rotation;
if (ActivePrefab.SwingFrequency > 0.0f)
@@ -199,11 +199,15 @@ namespace Barotrauma
if (LightSources != null)
{
Vector2 position2D = new Vector2(Position.X, Position.Y);
Vector2 camDiff = position2D - cam.WorldViewCenter;
for (int i = 0; i < LightSources.Length; i++)
{
if (LightSourceTriggers[i] != null) LightSources[i].Enabled = LightSourceTriggers[i].IsTriggered;
if (LightSourceTriggers[i] != null) { LightSources[i].Enabled = LightSourceTriggers[i].IsTriggered; }
LightSources[i].Rotation = -CurrentRotation;
LightSources[i].SpriteScale = CurrentScale;
LightSources[i].Position =
position2D - camDiff * Position.Z * LevelObjectManager.ParallaxStrength;
}
}
@@ -237,7 +241,10 @@ namespace Barotrauma
{
SoundChannels[i] = roundSound.Sound.Play(roundSound.Volume, roundSound.Range, roundSound.GetRandomFrequencyMultiplier(), soundPos);
}
SoundChannels[i].Position = new Vector3(soundPos.X, soundPos.Y, 0.0f);
if (SoundChannels[i] != null)
{
SoundChannels[i].Position = new Vector3(soundPos.X, soundPos.Y, 0.0f);
}
}
}
else if (SoundChannels[i] != null && SoundChannels[i].IsPlaying)
@@ -259,7 +266,7 @@ namespace Barotrauma
}
deformation.Update(deltaTime);
}
CurrentSpriteDeformation = SpriteDeformation.GetDeformation(spriteDeformations, ActivePrefab.DeformableSprite.Size);
CurrentSpriteDeformation = SpriteDeformation.GetDeformation(spriteDeformations, ActivePrefab.DeformableSprite.Size, flippedHorizontally: false);
if (LightSources != null)
{
foreach (LightSource lightSource in LightSources)

View File

@@ -10,9 +10,11 @@ namespace Barotrauma
{
partial class LevelObjectManager
{
private readonly List<LevelObject> visibleObjectsBack = new List<LevelObject>();
private readonly List<LevelObject> visibleObjectsMid = new List<LevelObject>();
private readonly List<LevelObject> visibleObjectsFront = new List<LevelObject>();
// Pre-initialized to the max size, so that we don't have to resize the lists at runtime. TODO: Could the capacity (of some collections?) be lower?
private readonly List<LevelObject> visibleObjectsBack = new List<LevelObject>(MaxVisibleObjects);
private readonly List<LevelObject> visibleObjectsMid = new List<LevelObject>(MaxVisibleObjects);
private readonly List<LevelObject> visibleObjectsFront = new List<LevelObject>(MaxVisibleObjects);
private readonly HashSet<LevelObject> allVisibleObjects = new HashSet<LevelObject>(MaxVisibleObjects);
private double NextRefreshTime;
@@ -31,25 +33,41 @@ namespace Barotrauma
visibleObjectsFront.Clear();
}
partial void UpdateProjSpecific(float deltaTime)
partial void UpdateProjSpecific(float deltaTime, Camera cam)
{
foreach (LevelObject obj in visibleObjectsBack)
{
obj.Update(deltaTime);
obj.Update(deltaTime, cam);
}
foreach (LevelObject obj in visibleObjectsMid)
{
obj.Update(deltaTime);
obj.Update(deltaTime, cam);
}
foreach (LevelObject obj in visibleObjectsFront)
{
obj.Update(deltaTime);
obj.Update(deltaTime, cam);
}
}
public IEnumerable<LevelObject> GetVisibleObjects()
/// <summary>
/// Returns all visible objects, but not in order, because internally uses a HashSet.
/// </summary>
public IEnumerable<LevelObject> GetAllVisibleObjects()
{
return visibleObjectsBack.Union(visibleObjectsMid).Union(visibleObjectsFront);
allVisibleObjects.Clear();
foreach (LevelObject obj in visibleObjectsBack)
{
allVisibleObjects.Add(obj);
}
foreach (LevelObject obj in visibleObjectsMid)
{
allVisibleObjects.Add(obj);
}
foreach (LevelObject obj in visibleObjectsFront)
{
allVisibleObjects.Add(obj);
}
return allVisibleObjects;
}
/// <summary>
@@ -207,7 +225,7 @@ namespace Barotrauma
activeSprite?.Draw(
spriteBatch,
new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength,
Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 3000.0f),
Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / obj.Prefab.FadeOutDepth),
activeSprite.Origin,
obj.CurrentRotation,
obj.CurrentScale,

Some files were not shown because too many files have changed in this diff Show More