v1.4.4.1 (Blood in the Water Update)
This commit is contained in:
@@ -109,7 +109,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition);
|
||||
float errorTolerance = character.CanMove && !character.IsRagdolled ? 0.01f : 0.2f;
|
||||
float errorTolerance = character.CanMove && (!character.IsRagdolled || character.AnimController.IsHangingWithRope) ? 0.01f : 0.2f;
|
||||
if (distSqrd > errorTolerance)
|
||||
{
|
||||
if (distSqrd > 10.0f || !character.CanMove)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Particles;
|
||||
@@ -9,7 +10,6 @@ using Microsoft.Xna.Framework.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -42,6 +42,8 @@ namespace Barotrauma
|
||||
private set;
|
||||
} = true;
|
||||
|
||||
public bool ShowInteractionLabels { get; private set; }
|
||||
|
||||
//the Character that the player is currently controlling
|
||||
private static Character controlled;
|
||||
|
||||
@@ -230,14 +232,51 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private float pressureEffectTimer;
|
||||
|
||||
private readonly List<ObjectiveEntity> activeObjectiveEntities = new List<ObjectiveEntity>();
|
||||
public IEnumerable<ObjectiveEntity> ActiveObjectiveEntities
|
||||
{
|
||||
get { return activeObjectiveEntities; }
|
||||
}
|
||||
|
||||
private static readonly List<SpeechBubble> speechBubbles = new List<SpeechBubble>();
|
||||
|
||||
private SpeechBubble textlessSpeechBubble;
|
||||
|
||||
sealed class SpeechBubble
|
||||
{
|
||||
public float LifeTime;
|
||||
public Vector2 PrevPosition;
|
||||
public Vector2 Position;
|
||||
public Vector2 DrawPosition;
|
||||
public float MoveUpAmount;
|
||||
public readonly string Text;
|
||||
public readonly Character Character;
|
||||
public readonly Submarine Submarine;
|
||||
public readonly Vector2 TextSize;
|
||||
|
||||
public Color Color;
|
||||
public bool Moving;
|
||||
|
||||
public SpeechBubble(Character character, float lifeTime, Color color, string text = "")
|
||||
{
|
||||
Text = ToolBox.WrapText(text, GUI.IntScale(300), GUIStyle.SmallFont.GetFontForStr(text));
|
||||
TextSize = GUIStyle.SmallFont.MeasureString(Text);
|
||||
|
||||
Character = character;
|
||||
Position = GetDesiredPosition();
|
||||
Submarine = character.Submarine;
|
||||
LifeTime = lifeTime;
|
||||
Color = color;
|
||||
}
|
||||
|
||||
public Vector2 GetDesiredPosition()
|
||||
{
|
||||
return Character.Position + Vector2.UnitY * 100;
|
||||
}
|
||||
}
|
||||
|
||||
private float pressureEffectTimer;
|
||||
|
||||
partial void InitProjSpecific(ContentXElement mainElement)
|
||||
{
|
||||
soundTimer = Rand.Range(0.0f, Params.SoundInterval);
|
||||
@@ -272,6 +311,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<Item> previousInteractablesInRange = new();
|
||||
private readonly List<Item> interactablesInRange = new();
|
||||
|
||||
private bool wasFiring;
|
||||
|
||||
/// <summary>
|
||||
@@ -279,6 +321,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public void ControlLocalPlayer(float deltaTime, Camera cam, bool moveCam = true)
|
||||
{
|
||||
|
||||
if (DisableControls || GUI.InputBlockingMenuOpen)
|
||||
{
|
||||
foreach (Key key in keys)
|
||||
@@ -316,6 +359,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
ShowInteractionLabels = keys[(int)InputType.ShowInteractionLabels].Held;
|
||||
|
||||
if (ShowInteractionLabels)
|
||||
{
|
||||
focusedItem = InteractionLabelManager.HoveredItem;
|
||||
}
|
||||
|
||||
//if we were firing (= pressing the aim and shoot keys at the same time)
|
||||
//and the fire key is the same as Select or Use, reset the key to prevent accidentally selecting/using items
|
||||
if (wasFiring && !keys[(int)InputType.Shoot].Held)
|
||||
@@ -549,19 +599,61 @@ namespace Barotrauma
|
||||
if (Lights.LightManager.ViewTarget == this) { Lights.LightManager.ViewTarget = null; }
|
||||
}
|
||||
|
||||
private void UpdateInteractablesInRange()
|
||||
{
|
||||
// keep two lists to detect changes to the current state of interactables in range
|
||||
previousInteractablesInRange.Clear();
|
||||
previousInteractablesInRange.AddRange(interactablesInRange);
|
||||
|
||||
interactablesInRange.Clear();
|
||||
|
||||
//use the list of visible entities if it exists
|
||||
var entityList = Submarine.VisibleEntities ?? Item.ItemList;
|
||||
|
||||
foreach (MapEntity entity in entityList)
|
||||
{
|
||||
if (entity is not Item item) { continue; }
|
||||
|
||||
if (item.body != null && !item.body.Enabled) { continue; }
|
||||
|
||||
if (item.ParentInventory != null) { continue; }
|
||||
|
||||
if (item.Prefab.RequireCampaignInteract &&
|
||||
item.CampaignInteractionType == CampaignMode.InteractionType.None)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Screen.Selected is SubEditorScreen { WiringMode: true } &&
|
||||
item.GetComponent<ConnectionPanel>() == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CanInteractWith(item))
|
||||
{
|
||||
interactablesInRange.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (!interactablesInRange.SequenceEqual(previousInteractablesInRange))
|
||||
{
|
||||
InteractionLabelManager.RefreshInteractablesInRange(interactablesInRange);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<Item> debugInteractablesInRange = new List<Item>();
|
||||
private readonly List<Item> debugInteractablesAtCursor = new List<Item>();
|
||||
private readonly List<(Item item, float dist)> debugInteractablesNearCursor = new List<(Item item, float dist)>();
|
||||
|
||||
/// <summary>
|
||||
/// Finds the front (lowest depth) interactable item at a position. "Interactable" in this case means that the character can "reach" the item.
|
||||
/// Finds the front (lowest depth) interactable item at a position. "Interactable" in this case means that the character can "reach" the item.
|
||||
/// </summary>
|
||||
/// <param name="character">The Character who is looking for the interactable item, only items that are close enough to this character are returned</param>
|
||||
/// <param name="simPosition">The item at the simPosition, with the lowest depth, is returned</param>
|
||||
/// <param name="allowFindingNearestItem">If this is true and an item cannot be found at simPosition then a nearest item will be returned if possible</param>
|
||||
/// <param name="hull">If a hull is specified, only items within that hull are returned</param>
|
||||
public Item FindItemAtPosition(Vector2 simPosition, float aimAssistModifier = 0.0f, Item[] ignoredItems = null)
|
||||
/// <param name="itemCollection">Item collection to look in</param>
|
||||
/// <param name="simPosition">sim position for distance comparison (such as mouse position)</param>
|
||||
/// <param name="aimAssistModifier">aim assist modifier</param>
|
||||
/// <returns></returns>
|
||||
public Item FindClosestItem(List<Item> itemCollection, Vector2 simPosition, float aimAssistModifier = 0.0f)
|
||||
{
|
||||
if (Submarine != null)
|
||||
{
|
||||
@@ -580,24 +672,11 @@ namespace Barotrauma
|
||||
float aimAssistAmount = SelectedItem == null ? 100.0f * aimAssistModifier : 1.0f;
|
||||
|
||||
Vector2 displayPosition = ConvertUnits.ToDisplayUnits(simPosition);
|
||||
|
||||
//use the list of visible entities if it exists
|
||||
var entityList = Submarine.VisibleEntities ?? Item.ItemList;
|
||||
|
||||
|
||||
Item closestItem = null;
|
||||
float closestItemDistance = Math.Max(aimAssistAmount, 2.0f);
|
||||
foreach (MapEntity entity in entityList)
|
||||
foreach (var item in itemCollection)
|
||||
{
|
||||
if (entity is not Item item)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (item.body != null && !item.body.Enabled) { continue; }
|
||||
if (item.ParentInventory != null) { continue; }
|
||||
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
|
||||
if (item.Prefab.RequireCampaignInteract && item.CampaignInteractionType == CampaignMode.InteractionType.None) { continue; }
|
||||
if (Screen.Selected is SubEditorScreen editor && editor.WiringMode && item.GetComponent<ConnectionPanel>() == null) { continue; }
|
||||
|
||||
if (draggingItemToWorld)
|
||||
{
|
||||
if (item.OwnInventory == null ||
|
||||
@@ -644,7 +723,7 @@ namespace Barotrauma
|
||||
distanceToItem = 2.0f + Vector2.Distance(rectIntersectionPoint, displayPosition);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (distanceToItem > closestItemDistance) { continue; }
|
||||
if (!CanInteractWith(item)) { continue; }
|
||||
|
||||
@@ -661,7 +740,7 @@ namespace Barotrauma
|
||||
Character closestCharacter = null;
|
||||
|
||||
maxDist = ConvertUnits.ToSimUnits(maxDist);
|
||||
float closestDist = maxDist * maxDist;
|
||||
float closestDist = maxDist;
|
||||
foreach (Character c in CharacterList)
|
||||
{
|
||||
if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; }
|
||||
@@ -704,6 +783,12 @@ namespace Barotrauma
|
||||
}
|
||||
guiMessages.RemoveAll(m => m.Timer >= m.Lifetime);
|
||||
|
||||
if (textlessSpeechBubble != null)
|
||||
{
|
||||
textlessSpeechBubble.LifeTime -= deltaTime;
|
||||
if (textlessSpeechBubble.LifeTime <= 0) { textlessSpeechBubble = null; }
|
||||
}
|
||||
|
||||
if (!enabled) { return; }
|
||||
|
||||
if (!IsIncapacitated)
|
||||
@@ -890,13 +975,6 @@ namespace Barotrauma
|
||||
|
||||
pos.Y = -pos.Y;
|
||||
|
||||
if (speechBubbleTimer > 0.0f)
|
||||
{
|
||||
GUIStyle.SpeechBubbleIcon.Value.Sprite.Draw(spriteBatch, pos - Vector2.UnitY * 5,
|
||||
speechBubbleColor * Math.Min(speechBubbleTimer, 1.0f), 0.0f,
|
||||
Math.Min(speechBubbleTimer, 1.0f));
|
||||
}
|
||||
|
||||
if (this == controlled)
|
||||
{
|
||||
if (DebugDrawInteract)
|
||||
@@ -921,113 +999,232 @@ namespace Barotrauma
|
||||
ToolBox.GradientLerp(dist, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green), width: 2);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float hoverRange = 300.0f;
|
||||
float fadeOutRange = 200.0f;
|
||||
float cursorDist = Vector2.Distance(WorldPosition, cam.ScreenToWorld(PlayerInput.MousePosition));
|
||||
float hudInfoAlpha =
|
||||
CampaignInteractionType == CampaignMode.InteractionType.None ?
|
||||
MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) :
|
||||
1.0f;
|
||||
|
||||
if (!GUI.DisableCharacterNames && hudInfoVisible &&
|
||||
(controlled == null || this != controlled.FocusedCharacter || IsPet) && cam.Zoom > 0.4f)
|
||||
else
|
||||
{
|
||||
if (info != null)
|
||||
|
||||
float hoverRange = 300.0f;
|
||||
float fadeOutRange = 200.0f;
|
||||
float cursorDist = Vector2.Distance(WorldPosition, cam.ScreenToWorld(PlayerInput.MousePosition));
|
||||
float hudInfoAlpha =
|
||||
CampaignInteractionType == CampaignMode.InteractionType.None ?
|
||||
MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) :
|
||||
1.0f;
|
||||
|
||||
if (!GUI.DisableCharacterNames && hudInfoVisible &&
|
||||
(controlled == null || this != controlled.FocusedCharacter || IsPet) && cam.Zoom > 0.4f)
|
||||
{
|
||||
LocalizedString name = Info.DisplayName;
|
||||
if (controlled == null && name != Info.Name)
|
||||
{
|
||||
name += " " + TextManager.Get("Disguised");
|
||||
}
|
||||
else if (Info.Title != null && TeamID != CharacterTeamType.Team1)
|
||||
if (info != null)
|
||||
{
|
||||
name += '\n' + Info.Title;
|
||||
LocalizedString name = Info.DisplayName;
|
||||
if (controlled == null && name != Info.Name)
|
||||
{
|
||||
name += " " + TextManager.Get("Disguised");
|
||||
}
|
||||
else if (Info.Title != null && TeamID != CharacterTeamType.Team1)
|
||||
{
|
||||
name += '\n' + Info.Title;
|
||||
}
|
||||
|
||||
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;
|
||||
Color nameColor = GetNameColor();
|
||||
|
||||
Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
|
||||
Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height);
|
||||
namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y;
|
||||
namePos *= screenSize / viewportSize;
|
||||
namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y);
|
||||
namePos *= viewportSize / screenSize;
|
||||
namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y;
|
||||
|
||||
if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract)
|
||||
{
|
||||
var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType);
|
||||
if (iconStyle != null)
|
||||
{
|
||||
Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f;
|
||||
Vector2 iconPos = headPos;
|
||||
iconPos.Y = -iconPos.Y;
|
||||
nameColor = iconStyle.Color;
|
||||
var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First();
|
||||
float iconScale = (30.0f / icon.Sprite.size.X / cam.Zoom) * GUI.Scale;
|
||||
icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (GameMain.DebugDraw)
|
||||
{
|
||||
GUIStyle.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
Color nameColor = GetNameColor();
|
||||
|
||||
Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
|
||||
Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height);
|
||||
namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y;
|
||||
namePos *= screenSize / viewportSize;
|
||||
namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y);
|
||||
namePos *= viewportSize / screenSize;
|
||||
namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y;
|
||||
|
||||
if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract)
|
||||
var petBehavior = (AIController as EnemyAIController)?.PetBehavior;
|
||||
if (petBehavior != null && !IsDead && !IsUnconscious)
|
||||
{
|
||||
var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType);
|
||||
var petStatus = petBehavior.GetCurrentStatusIndicatorType();
|
||||
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;
|
||||
nameColor = iconStyle.Color;
|
||||
var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First();
|
||||
float iconScale = (30.0f / icon.Sprite.size.X / cam.Zoom) * GUI.Scale;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (GameMain.DebugDraw)
|
||||
if (IsDead) { return; }
|
||||
|
||||
var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars;
|
||||
if (healthBarMode != EnemyHealthBarMode.ShowAll)
|
||||
{
|
||||
if (Controlled == null)
|
||||
{
|
||||
GUIStyle.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White);
|
||||
if (!IsOnPlayerTeam) { return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!HumanAIController.IsFriendly(Controlled, this) ||
|
||||
(AIController is HumanAIController humanAi && humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective && HumanAIController.IsFriendly(Controlled, combatObjective.Enemy)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var petBehavior = (AIController as EnemyAIController)?.PetBehavior;
|
||||
if (petBehavior != null && !IsDead && !IsUnconscious)
|
||||
|
||||
if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible)
|
||||
{
|
||||
var petStatus = petBehavior.GetCurrentStatusIndicatorType();
|
||||
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);
|
||||
}
|
||||
hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f));
|
||||
|
||||
Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y);
|
||||
GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f),
|
||||
CharacterHealth.DisplayedVitality / MaxVitality,
|
||||
Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha,
|
||||
new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
if (IsDead) { return; }
|
||||
|
||||
var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars;
|
||||
if (healthBarMode != EnemyHealthBarMode.ShowAll)
|
||||
if (textlessSpeechBubble != null)
|
||||
{
|
||||
if (Controlled == null)
|
||||
Vector2 iconPos = pos - Vector2.UnitY * 5;
|
||||
|
||||
GUIStyle.SpeechBubbleIcon.Value.Sprite.Draw(spriteBatch, iconPos,
|
||||
textlessSpeechBubble.Color * Math.Min(textlessSpeechBubble.LifeTime, 1.0f), 0.0f,
|
||||
Math.Min(textlessSpeechBubble.LifeTime, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowSpeechBubble(Color color, string text)
|
||||
{
|
||||
if (!GameSettings.CurrentConfig.ChatSpeechBubbles)
|
||||
{
|
||||
ShowTextlessSpeechBubble(1.0f, color);
|
||||
return;
|
||||
}
|
||||
float duration = MathHelper.Lerp(1.0f, 8.0f, Math.Min(text.Length / 100.0f, 1.0f));
|
||||
speechBubbles.Add(new SpeechBubble(this, duration, color, text));
|
||||
textlessSpeechBubble = null;
|
||||
}
|
||||
|
||||
public void ShowTextlessSpeechBubble(float duration, Color color)
|
||||
{
|
||||
if (speechBubbles.Any(sb => sb.Character == this)) { return; }
|
||||
if (textlessSpeechBubble == null)
|
||||
{
|
||||
textlessSpeechBubble = new SpeechBubble(this, duration, color);
|
||||
}
|
||||
else
|
||||
{
|
||||
textlessSpeechBubble.Color = color;
|
||||
textlessSpeechBubble.LifeTime = Math.Max(textlessSpeechBubble.LifeTime, duration);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawSpeechBubbles(SpriteBatch spriteBatch, Camera cam)
|
||||
{
|
||||
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform);
|
||||
foreach (var bubble in speechBubbles)
|
||||
{
|
||||
Vector2 iconPos = Timing.Interpolate(bubble.PrevPosition, bubble.Position);
|
||||
iconPos += Vector2.UnitY * bubble.MoveUpAmount;
|
||||
if (bubble.Submarine != null)
|
||||
{
|
||||
if (!IsOnPlayerTeam) { return; }
|
||||
iconPos += bubble.Submarine.DrawPosition;
|
||||
}
|
||||
else
|
||||
|
||||
float alpha = 1.0f;
|
||||
float mouseDist = Vector2.Distance(cam.WorldToScreen(iconPos), PlayerInput.MousePosition);
|
||||
//treat the size of the bubble from corner to corner as the
|
||||
float textSize = bubble.TextSize.Length();
|
||||
if (mouseDist < textSize)
|
||||
{
|
||||
if (!HumanAIController.IsFriendly(Controlled, this) ||
|
||||
(AIController is HumanAIController humanAi && humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective && HumanAIController.IsFriendly(Controlled, combatObjective.Enemy)))
|
||||
{
|
||||
return;
|
||||
alpha *= Math.Max(mouseDist / textSize, 0.5f);
|
||||
}
|
||||
|
||||
iconPos.Y = -iconPos.Y;
|
||||
if (GUIStyle.SpeechBubbleIconSliced.Value is { } speechBubbleIconSliced)
|
||||
{
|
||||
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);
|
||||
}
|
||||
spriteBatch.End();
|
||||
}
|
||||
|
||||
static partial void UpdateSpeechBubbles(float deltaTime)
|
||||
{
|
||||
for (int i = speechBubbles.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var bubble = speechBubbles[i];
|
||||
bubble.LifeTime -= deltaTime;
|
||||
if (bubble.LifeTime <= 0 || bubble.Character is { Removed: true })
|
||||
{
|
||||
speechBubbles.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
bubble.PrevPosition = bubble.Position;
|
||||
|
||||
Vector2 desiredPos = bubble.GetDesiredPosition();
|
||||
Vector2 diff = desiredPos - bubble.Position;
|
||||
float dist = diff.Length();
|
||||
//how far the bubble needs to be from the desired position to start moving
|
||||
const float MoveThreshold = 100.0f;
|
||||
const float MaxSpeed = 1000.0f;
|
||||
if (dist < 1)
|
||||
{
|
||||
bubble.Moving = false;
|
||||
}
|
||||
else if (dist > MoveThreshold || bubble.Moving)
|
||||
{
|
||||
Vector2 moveAmount = diff / dist * MathHelper.Clamp(dist * 5, 0, MaxSpeed) * deltaTime;
|
||||
//slower vertical movement (don't want to interfere too much with the bubbles floating up
|
||||
//and the overlap prevention which works vertically)
|
||||
moveAmount.Y *= 0.1f;
|
||||
bubble.Position += moveAmount;
|
||||
bubble.Moving = true;
|
||||
}
|
||||
|
||||
bubble.MoveUpAmount += deltaTime * 5.0f;
|
||||
//go through the newer bubbles, move this one out of the way if one is overlapping
|
||||
for (int j = i + 1; j < speechBubbles.Count; j++)
|
||||
{
|
||||
var otherBubble = speechBubbles[j];
|
||||
{
|
||||
if (Math.Abs(bubble.Position.X - otherBubble.Position.X) < (bubble.TextSize.X + otherBubble.TextSize.X) / 2 &&
|
||||
Math.Abs(bubble.Position.Y - otherBubble.Position.Y) < (bubble.TextSize.Y + otherBubble.TextSize.Y) / 2 + 10)
|
||||
{
|
||||
bubble.Position += Vector2.UnitY * deltaTime * 50.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible)
|
||||
{
|
||||
hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f));
|
||||
|
||||
Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y);
|
||||
GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f),
|
||||
CharacterHealth.DisplayedVitality / MaxVitality,
|
||||
Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha,
|
||||
new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
public Color GetNameColor()
|
||||
|
||||
@@ -207,7 +207,7 @@ namespace Barotrauma
|
||||
{
|
||||
cachedHudTexts.Clear();
|
||||
}
|
||||
Identifier key = (textTag + keyBind).ToIdentifier();
|
||||
Identifier key = (textTag + keyBind + GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).ToIdentifier();
|
||||
if (cachedHudTexts.TryGetValue(key, out LocalizedString text)) { return text; }
|
||||
text = TextManager.GetWithVariable(textTag, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).Value;
|
||||
cachedHudTexts.Add(key, text);
|
||||
@@ -246,6 +246,7 @@ namespace Barotrauma
|
||||
|
||||
public static void Update(float deltaTime, Character character, Camera cam)
|
||||
{
|
||||
|
||||
UpdateBossProgressBars(deltaTime);
|
||||
|
||||
if (GUI.DisableHUD)
|
||||
@@ -256,6 +257,11 @@ namespace Barotrauma
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (character.ShowInteractionLabels && character.ViewTarget == null)
|
||||
{
|
||||
InteractionLabelManager.Update(character, cam);
|
||||
}
|
||||
|
||||
if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen)
|
||||
{
|
||||
@@ -375,7 +381,7 @@ namespace Barotrauma
|
||||
|
||||
static bool DrawIcon(Order o) =>
|
||||
o != null &&
|
||||
(!(o.TargetEntity is Item i) ||
|
||||
(o.TargetEntity is not Item i ||
|
||||
o.DrawIconWhenContained ||
|
||||
i.GetRootInventoryOwner() == i);
|
||||
}
|
||||
@@ -405,90 +411,115 @@ namespace Barotrauma
|
||||
if (!brokenItem.IsInteractable(character)) { continue; }
|
||||
float alpha = GetDistanceBasedIconAlpha(brokenItem);
|
||||
if (alpha <= 0.0f) { continue; }
|
||||
GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite,
|
||||
GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite,
|
||||
Color.Lerp(GUIStyle.Red, GUIStyle.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha);
|
||||
}
|
||||
|
||||
if (OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder))
|
||||
{
|
||||
foreach (Item deconstructItem in Item.DeconstructItems)
|
||||
{
|
||||
if (deconstructItem.ParentInventory != null) { continue; }
|
||||
if (deconstructItem.OrderedToBeIgnored) { continue; }
|
||||
if (deconstructItem.Submarine != character.Submarine || !deconstructItem.IsInteractable(character)) { continue; }
|
||||
float alpha = GetDistanceBasedIconAlpha(deconstructItem, maxDistance: 450) * 0.7f;
|
||||
if (alpha <= 0.0f) { continue; }
|
||||
GUI.DrawIndicator(spriteBatch, deconstructItem.DrawPosition, cam, 100.0f, deconstructOrder.SymbolSprite,
|
||||
GUIStyle.Red, scaleMultiplier: 0.5f, overrideAlpha: alpha);
|
||||
}
|
||||
}
|
||||
|
||||
float GetDistanceBasedIconAlpha(ISpatialEntity target, float maxDistance = 1000.0f)
|
||||
{
|
||||
float dist = Vector2.Distance(character.WorldPosition, target.WorldPosition);
|
||||
return Math.Min((maxDistance - dist) / maxDistance * 2.0f, 1.0f);
|
||||
}
|
||||
|
||||
|
||||
if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen && (!character.IsKeyDown(InputType.Aim) || character.HeldItems.None(it => it?.GetComponent<Sprayer>() != null)))
|
||||
{
|
||||
if (character.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected)
|
||||
if (!character.ShowInteractionLabels)
|
||||
{
|
||||
DrawCharacterHoverTexts(spriteBatch, cam, character);
|
||||
}
|
||||
|
||||
if (character.FocusedItem != null)
|
||||
{
|
||||
if (focusedItem != character.FocusedItem)
|
||||
if (character.FocusedCharacter is { CanBeSelected: true })
|
||||
{
|
||||
focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer);
|
||||
RecreateHudTexts = true;
|
||||
DrawCharacterHoverTexts(spriteBatch, cam, character);
|
||||
}
|
||||
focusedItem = character.FocusedItem;
|
||||
}
|
||||
|
||||
if (focusedItem != null && focusedItemOverlayTimer > ItemOverlayDelay)
|
||||
{
|
||||
Vector2 circlePos = cam.WorldToScreen(focusedItem.DrawPosition);
|
||||
float circleSize = Math.Max(focusedItem.Rect.Width, focusedItem.Rect.Height) * 1.5f;
|
||||
circleSize = MathHelper.Clamp(circleSize, 45.0f, 100.0f) * Math.Min((focusedItemOverlayTimer - 1.0f) * 5.0f, 1.0f);
|
||||
if (circleSize > 0.0f)
|
||||
|
||||
if (character.FocusedItem != null)
|
||||
{
|
||||
Vector2 scale = new Vector2(circleSize / GUIStyle.FocusIndicator.FrameSize.X);
|
||||
GUIStyle.FocusIndicator.Draw(spriteBatch,
|
||||
(int)((focusedItemOverlayTimer - 1.0f) * GUIStyle.FocusIndicator.FrameCount * 3.0f),
|
||||
circlePos,
|
||||
Color.LightBlue * 0.3f,
|
||||
origin: GUIStyle.FocusIndicator.FrameSize.ToVector2() / 2,
|
||||
rotate: (float)Timing.TotalTime,
|
||||
scale: scale);
|
||||
}
|
||||
|
||||
if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld)
|
||||
{
|
||||
bool hudTextsContextual = PlayerInput.IsShiftDown();
|
||||
if (RecreateHudTexts || lastHudTextsContextual != hudTextsContextual)
|
||||
if (focusedItem != character.FocusedItem)
|
||||
{
|
||||
focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer);
|
||||
RecreateHudTexts = true;
|
||||
lastHudTextsContextual = hudTextsContextual;
|
||||
}
|
||||
var hudTexts = focusedItem.GetHUDTexts(character, RecreateHudTexts);
|
||||
RecreateHudTexts = false;
|
||||
|
||||
int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X);
|
||||
|
||||
Vector2 textSize = GUIStyle.Font.MeasureString(hudTexts.First().Text);
|
||||
Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString(hudTexts.First().Text);
|
||||
|
||||
Vector2 startPos = cam.WorldToScreen(focusedItem.DrawPosition);
|
||||
startPos.Y -= (hudTexts.Count + 1) * textSize.Y;
|
||||
if (focusedItem.Sprite != null)
|
||||
focusedItem = character.FocusedItem;
|
||||
}
|
||||
|
||||
if (focusedItem != null && focusedItemOverlayTimer > ItemOverlayDelay)
|
||||
{
|
||||
Vector2 circlePos = cam.WorldToScreen(focusedItem.DrawPosition);
|
||||
float circleSize = Math.Max(focusedItem.Rect.Width, focusedItem.Rect.Height) * 1.5f;
|
||||
circleSize = MathHelper.Clamp(circleSize, 45.0f, 100.0f) * Math.Min((focusedItemOverlayTimer - 1.0f) * 5.0f, 1.0f);
|
||||
if (circleSize > 0.0f)
|
||||
{
|
||||
startPos.X += (int)(circleSize * 0.4f * dir);
|
||||
startPos.Y -= (int)(circleSize * 0.4f);
|
||||
Vector2 scale = new Vector2(circleSize / GUIStyle.FocusIndicator.FrameSize.X);
|
||||
GUIStyle.FocusIndicator.Draw(spriteBatch,
|
||||
(int)((focusedItemOverlayTimer - 1.0f) * GUIStyle.FocusIndicator.FrameCount * 3.0f),
|
||||
circlePos,
|
||||
Color.LightBlue * 0.3f,
|
||||
origin: GUIStyle.FocusIndicator.FrameSize.ToVector2() / 2,
|
||||
rotate: (float)Timing.TotalTime,
|
||||
scale: scale);
|
||||
}
|
||||
|
||||
Vector2 textPos = startPos;
|
||||
if (dir == -1) { textPos.X -= largeTextSize.X; }
|
||||
|
||||
float alpha = MathHelper.Clamp((focusedItemOverlayTimer - ItemOverlayDelay) * 2.0f, 0.0f, 1.0f);
|
||||
|
||||
GUI.DrawString(spriteBatch, textPos, hudTexts.First().Text, hudTexts.First().Color * alpha, Color.Black * alpha * 0.7f, 2, font: GUIStyle.SubHeadingFont, ForceUpperCase.No);
|
||||
startPos.X += dir * 10.0f * GUI.Scale;
|
||||
textPos.X += dir * 10.0f * GUI.Scale;
|
||||
textPos.Y += largeTextSize.Y;
|
||||
foreach (ColoredText coloredText in hudTexts.Skip(1))
|
||||
if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld)
|
||||
{
|
||||
if (dir == -1) textPos.X = (int)(startPos.X - GUIStyle.SmallFont.MeasureString(coloredText.Text).X);
|
||||
GUI.DrawString(spriteBatch, textPos, coloredText.Text, coloredText.Color * alpha, Color.Black * alpha * 0.7f, 2, GUIStyle.SmallFont);
|
||||
textPos.Y += textSize.Y;
|
||||
}
|
||||
}
|
||||
bool hudTextsContextual = PlayerInput.KeyDown(InputType.ContextualCommand);
|
||||
if (RecreateHudTexts || lastHudTextsContextual != hudTextsContextual)
|
||||
{
|
||||
RecreateHudTexts = true;
|
||||
lastHudTextsContextual = hudTextsContextual;
|
||||
}
|
||||
var hudTexts = focusedItem.GetHUDTexts(character, RecreateHudTexts);
|
||||
RecreateHudTexts = false;
|
||||
|
||||
int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X);
|
||||
|
||||
Vector2 textSize = GUIStyle.Font.MeasureString(hudTexts.First().Text);
|
||||
Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString(hudTexts.First().Text);
|
||||
|
||||
Vector2 startPos = cam.WorldToScreen(focusedItem.DrawPosition);
|
||||
startPos.Y -= (hudTexts.Count + 1) * textSize.Y;
|
||||
if (focusedItem.Sprite != null)
|
||||
{
|
||||
startPos.X += (int)(circleSize * 0.4f * dir);
|
||||
startPos.Y -= (int)(circleSize * 0.4f);
|
||||
}
|
||||
|
||||
Vector2 textPos = startPos;
|
||||
if (dir == -1) { textPos.X -= largeTextSize.X; }
|
||||
|
||||
float alpha = MathHelper.Clamp((focusedItemOverlayTimer - ItemOverlayDelay) * 2.0f, 0.0f, 1.0f);
|
||||
|
||||
GUI.DrawString(spriteBatch, textPos, hudTexts.First().Text, hudTexts.First().Color * alpha, Color.Black * alpha * 0.7f, 2, font: GUIStyle.SubHeadingFont, ForceUpperCase.No);
|
||||
startPos.X += dir * 10.0f * GUI.Scale;
|
||||
textPos.X += dir * 10.0f * GUI.Scale;
|
||||
textPos.Y += largeTextSize.Y;
|
||||
foreach (ColoredText coloredText in hudTexts.Skip(1))
|
||||
{
|
||||
if (dir == -1)
|
||||
{
|
||||
textPos.X = (int)(startPos.X - GUIStyle.SmallFont.MeasureString(coloredText.Text).X);
|
||||
}
|
||||
GUI.DrawString(spriteBatch, textPos, coloredText.Text, coloredText.Color * alpha, Color.Black * alpha * 0.7f, 2, GUIStyle.SmallFont);
|
||||
textPos.Y += textSize.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (character.ShowInteractionLabels && character.ViewTarget == null)
|
||||
{
|
||||
InteractionLabelManager.DrawLabels(spriteBatch, cam, character);
|
||||
}
|
||||
|
||||
foreach (HUDProgressBar progressBar in character.HUDProgressBars.Values)
|
||||
@@ -673,6 +704,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static bool MouseOnCharacterPortrait()
|
||||
{
|
||||
if (Character.Controlled == null) { return false; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Linq;
|
||||
@@ -46,6 +46,7 @@ namespace Barotrauma
|
||||
ContentPath tintMaskPath = maskElement.GetAttributeContentPath("texture");
|
||||
if (!tintMaskPath.IsNullOrEmpty())
|
||||
{
|
||||
VerifySpriteTagsLoaded();
|
||||
tintMask = new Sprite(maskElement, file: Limb.GetSpritePath(tintMaskPath, this));
|
||||
tintHighlightThreshold = maskElement.GetAttributeFloat("highlightthreshold", 0.6f);
|
||||
tintHighlightMultiplier = maskElement.GetAttributeFloat("highlightmultiplier", 0.8f);
|
||||
@@ -61,7 +62,7 @@ namespace Barotrauma
|
||||
//Stretch = true
|
||||
};
|
||||
|
||||
var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.322f), paddedFrame.RectTransform), isHorizontal: true);
|
||||
var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), paddedFrame.RectTransform), isHorizontal: true);
|
||||
|
||||
new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform),
|
||||
onDraw: (sb, component) => DrawInfoFrameCharacterIcon(sb, component.Rect));
|
||||
@@ -185,7 +186,7 @@ namespace Barotrauma
|
||||
|
||||
private void DrawInfoFrameCharacterIcon(SpriteBatch sb, Rectangle componentRect)
|
||||
{
|
||||
if (_headSprite == null) { return; }
|
||||
if (HeadSprite == null) { return; }
|
||||
Vector2 targetAreaSize = componentRect.Size.ToVector2();
|
||||
float scale = Math.Min(targetAreaSize.X / _headSprite.size.X, targetAreaSize.Y / _headSprite.size.Y);
|
||||
DrawIcon(sb, componentRect.Location.ToVector2() + _headSprite.size / 2 * scale, targetAreaSize);
|
||||
@@ -537,8 +538,7 @@ namespace Barotrauma
|
||||
Color skinColor = inc.ReadColorR8G8B8();
|
||||
Color hairColor = inc.ReadColorR8G8B8();
|
||||
Color facialHairColor = inc.ReadColorR8G8B8();
|
||||
|
||||
string ragdollFile = inc.ReadString();
|
||||
|
||||
Identifier npcId = inc.ReadIdentifier();
|
||||
|
||||
Identifier factionId = inc.ReadIdentifier();
|
||||
@@ -560,18 +560,15 @@ namespace Barotrauma
|
||||
throw new Exception($"Error while reading {nameof(CharacterInfo)} received from the server: could not find a job prefab with the identifier \"{jobIdentifier}\".");
|
||||
}
|
||||
byte skillCount = inc.ReadByte();
|
||||
List<SkillPrefab> jobSkills = jobPrefab?.Skills.OrderBy(s => s.Identifier).ToList();
|
||||
for (int i = 0; i < skillCount; i++)
|
||||
{
|
||||
Identifier skillIdentifier = inc.ReadIdentifier();
|
||||
float skillLevel = inc.ReadSingle();
|
||||
if (jobSkills != null && i < jobSkills.Count)
|
||||
{
|
||||
skillLevels.Add(jobSkills[i].Identifier, skillLevel);
|
||||
}
|
||||
skillLevels.Add(skillIdentifier, skillLevel);
|
||||
}
|
||||
}
|
||||
|
||||
CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant, npcIdentifier: npcId)
|
||||
CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, variant, npcIdentifier: npcId)
|
||||
{
|
||||
ID = infoID,
|
||||
MinReputationToHire = (factionId, minReputationToHire)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
@@ -532,6 +533,47 @@ namespace Barotrauma
|
||||
bool removeOnDeath = msg.ReadBoolean();
|
||||
info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true);
|
||||
}
|
||||
break;
|
||||
case EventType.LatchOntoTarget:
|
||||
bool attached = msg.ReadBoolean();
|
||||
if (attached)
|
||||
{
|
||||
Vector2 characterSimPos = new Vector2(msg.ReadSingle(), msg.ReadSingle());
|
||||
Vector2 attachSurfaceNormal = new Vector2(msg.ReadSingle(), msg.ReadSingle());
|
||||
Vector2 attachPos = new Vector2(msg.ReadSingle(), msg.ReadSingle());
|
||||
int attachWallIndex = msg.ReadInt32();
|
||||
UInt16 attachTargetId = msg.ReadUInt16();
|
||||
|
||||
if (AIController is EnemyAIController { LatchOntoAI: { } latchOntoAi })
|
||||
{
|
||||
var attachTargetEntity = FindEntityByID(attachTargetId);
|
||||
switch (attachTargetEntity)
|
||||
{
|
||||
case Character attachTargetCharacter:
|
||||
latchOntoAi.SetAttachTarget(attachTargetCharacter);
|
||||
break;
|
||||
case Structure attachTargetStructure:
|
||||
latchOntoAi.SetAttachTarget(attachTargetStructure, attachPos, attachSurfaceNormal);
|
||||
break;
|
||||
default:
|
||||
var allLevelWalls = Level.Loaded.GetAllCells();
|
||||
if (attachWallIndex >= 0 && attachWallIndex <= allLevelWalls.Count)
|
||||
{
|
||||
latchOntoAi.SetAttachTarget(allLevelWalls[attachWallIndex]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
latchOntoAi.AttachToBody(attachPos, attachSurfaceNormal, characterSimPos);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (AIController is EnemyAIController { LatchOntoAI: { } latchOntoAi })
|
||||
{
|
||||
latchOntoAi.DeattachFromBody(reset: false);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
msg.ReadPadBits();
|
||||
|
||||
@@ -858,8 +858,8 @@ namespace Barotrauma
|
||||
foreach (GUIComponent component in recommendedTreatmentContainer.Content.Children)
|
||||
{
|
||||
var treatmentButton = component.GetChild<GUIButton>();
|
||||
if (!(treatmentButton?.UserData is ItemPrefab itemPrefab)) { continue; }
|
||||
var matchingItem = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true);
|
||||
if (treatmentButton?.UserData is not ItemPrefab itemPrefab) { continue; }
|
||||
var matchingItem = AIObjectiveRescue.FindMedicalItem(Character.Controlled.Inventory, itemPrefab.Identifier);
|
||||
treatmentButton.Enabled = matchingItem != null;
|
||||
if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover)
|
||||
{
|
||||
@@ -1387,7 +1387,6 @@ namespace Barotrauma
|
||||
//float = suitability
|
||||
Dictionary<Identifier, float> treatmentSuitability = new Dictionary<Identifier, float>();
|
||||
GetSuitableTreatments(treatmentSuitability,
|
||||
normalize: true,
|
||||
user: Character.Controlled,
|
||||
ignoreHiddenAfflictions: true,
|
||||
limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex));
|
||||
@@ -1421,9 +1420,12 @@ namespace Barotrauma
|
||||
int count = 0;
|
||||
foreach (KeyValuePair<Identifier, float> treatment in treatmentSuitabilities)
|
||||
{
|
||||
//don't list negative treatments
|
||||
if (treatment.Value < 0) { continue; }
|
||||
|
||||
count++;
|
||||
if (count > 5) { break; }
|
||||
if (!(MapEntityPrefab.Find(name: null, identifier: treatment.Key, showErrorMessages: false) is ItemPrefab item)) { continue; }
|
||||
if (MapEntityPrefab.FindByIdentifier(treatment.Key) is not ItemPrefab item) { continue; }
|
||||
|
||||
var itemSlot = new GUIFrame(new RectTransform(new Vector2(1.0f / 6.0f, 1.0f), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopLeft),
|
||||
style: null)
|
||||
@@ -1439,7 +1441,7 @@ namespace Barotrauma
|
||||
OnClicked = (btn, userdata) =>
|
||||
{
|
||||
if (userdata is not ItemPrefab itemPrefab) { return false; }
|
||||
var item = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true);
|
||||
var item = AIObjectiveRescue.FindMedicalItem(Character.Controlled.Inventory, it => it.Prefab == itemPrefab);
|
||||
if (item == null) { return false; }
|
||||
Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex);
|
||||
item.ApplyTreatment(Character.Controlled, Character, targetLimb);
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
#nullable enable
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static class InteractionLabelManager
|
||||
{
|
||||
|
||||
private class LabelData
|
||||
{
|
||||
private readonly Camera drawCamera;
|
||||
public readonly Item Item;
|
||||
|
||||
public RectangleF TextRect { get; set; }
|
||||
|
||||
public readonly Vector2 OriginalItemPosition;
|
||||
|
||||
public bool OverlapPreventionDone;
|
||||
|
||||
public LabelData(Item item, RectangleF textRect, Camera drawCamera)
|
||||
{
|
||||
Item = item;
|
||||
TextRect = textRect;
|
||||
OriginalItemPosition = item.Position;
|
||||
this.drawCamera = drawCamera;
|
||||
}
|
||||
|
||||
public RectangleF GetScreenDrawRect(Camera cam)
|
||||
{
|
||||
float scale = cam.Zoom;
|
||||
RectangleF screenDrawRect = TextRect;
|
||||
screenDrawRect.Location = drawCamera
|
||||
.WorldToScreen(screenDrawRect.Location + (Item.Submarine?.DrawPosition ?? Vector2.Zero));
|
||||
|
||||
return new RectangleF(
|
||||
screenDrawRect.X,
|
||||
screenDrawRect.Y,
|
||||
screenDrawRect.Width * scale,
|
||||
screenDrawRect.Height * scale);
|
||||
}
|
||||
|
||||
public Vector2 GetInteractableDrawPositionScreen()
|
||||
{
|
||||
return drawCamera.WorldToScreen(Item.DrawPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly List<LabelData> labels = new();
|
||||
|
||||
private const int TextBoxMarginPx = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier on the scale of the labels. Ad-hoc formula: since the zoom affects the size of the labels,
|
||||
/// and high resolutions are more zoomed in to keep the view range the same, let's scale down the labels on large resolutions to compensate.
|
||||
/// </summary>
|
||||
private static float LabelScale => 1.0f / GUI.Scale;
|
||||
|
||||
private static InteractionLabelDisplayMode displayMode;
|
||||
|
||||
private static int graphicsWidth, graphicsHeight;
|
||||
|
||||
private static bool shouldRecalculate;
|
||||
private static bool recalculateEverything;
|
||||
|
||||
private static readonly List<Item> interactablesInRange = new();
|
||||
|
||||
internal static Item? HoveredItem { get; private set; }
|
||||
|
||||
internal static void RefreshInteractablesInRange(List<Item> interactables)
|
||||
{
|
||||
interactablesInRange.Clear();
|
||||
interactablesInRange.AddRange(interactables);
|
||||
|
||||
shouldRecalculate = true;
|
||||
}
|
||||
|
||||
private static void RecalculateLabelPositions(Camera cam, Character character)
|
||||
{
|
||||
if (recalculateEverything)
|
||||
{
|
||||
labels.Clear();
|
||||
recalculateEverything = false;
|
||||
}
|
||||
|
||||
labels.RemoveAll(l => !interactablesInRange.Contains(l.Item));
|
||||
|
||||
// for every interactable, create a label data object with relevant info for real-time drawing
|
||||
foreach (var interactableInRange in interactablesInRange)
|
||||
{
|
||||
// this removes the hidden vents from the list
|
||||
if (interactableInRange.HasTag(Tags.HiddenItemContainer)) { continue; }
|
||||
|
||||
// filter out items depending on visibility filter setting
|
||||
switch (displayMode)
|
||||
{
|
||||
case InteractionLabelDisplayMode.InteractionAvailable when !interactableInRange.HasVisibleInteraction(character):
|
||||
case InteractionLabelDisplayMode.LooseItems when !IsLooseItem(interactableInRange):
|
||||
continue;
|
||||
}
|
||||
|
||||
RectangleF textRect = GetLabelRect(interactableInRange, cam);
|
||||
|
||||
if (labels.None(l => l.Item == interactableInRange))
|
||||
{
|
||||
var labelData = new LabelData(interactableInRange, textRect, cam);
|
||||
labels.Add(labelData);
|
||||
}
|
||||
}
|
||||
|
||||
PreventInteractionLabelOverlap(centerPos: character.Position);
|
||||
}
|
||||
|
||||
private static bool IsLooseItem(Item item)
|
||||
{
|
||||
bool hasActivePhysics = item.body is { Enabled: true };
|
||||
bool hasPickableComponent = item.GetComponent<Pickable>() != null;
|
||||
return hasActivePhysics && hasPickableComponent;
|
||||
}
|
||||
|
||||
private static RectangleF GetLabelRect(Item item, Camera cam)
|
||||
{
|
||||
// create rectangle for overlap prevention
|
||||
Vector2 itemTextSizeScreen = GUIStyle.SubHeadingFont.MeasureString(item.Name) * 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
|
||||
textRect.X -= textRect.Width / 2;
|
||||
textRect.Y += textRect.Height / 2;
|
||||
|
||||
// inflate by a bit, because the text is drawn with padding
|
||||
textRect.Inflate(TextBoxMarginPx * LabelScale, TextBoxMarginPx * LabelScale);
|
||||
|
||||
// the rect has screen space size, and sub-relative position
|
||||
textRect.Location = cam.ScreenToWorld(textRect.Location);
|
||||
return textRect;
|
||||
}
|
||||
|
||||
private static void PreventInteractionLabelOverlap(Vector2 centerPos)
|
||||
{
|
||||
//sort by distance from "centerPos": moving labels further away from the character (or whatever the center is) is preferred
|
||||
labels.Sort((l1, l2) =>
|
||||
Vector2.DistanceSquared(l1.TextRect.Center, centerPos).CompareTo(
|
||||
Vector2.DistanceSquared(l2.TextRect.Center, centerPos)));
|
||||
|
||||
const float MoveStep = 10.0f;
|
||||
bool intersections = true;
|
||||
int iterations = 0;
|
||||
int maxIterations = System.Math.Max(labels.Count * labels.Count, 100);
|
||||
|
||||
while (intersections && iterations < maxIterations)
|
||||
{
|
||||
intersections = false;
|
||||
foreach (var label in labels)
|
||||
{
|
||||
if (label.OverlapPreventionDone) { continue; }
|
||||
foreach (var otherLabel in labels)
|
||||
{
|
||||
if (label == otherLabel) { continue; }
|
||||
|
||||
//allow labels to overlap if there's multiple instances of the same item at (roughly) the same position
|
||||
if (label.Item.Prefab == otherLabel.Item.Prefab &&
|
||||
Vector2.DistanceSquared(label.Item.WorldPosition, otherLabel.Item.WorldPosition) < 1.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!label.TextRect.Intersects(otherLabel.TextRect))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
intersections = true;
|
||||
Vector2 moveAmount = Vector2.Normalize(label.TextRect.Center - centerPos) * MoveStep;
|
||||
label.TextRect = new RectangleF(label.TextRect.Location + moveAmount, label.TextRect.Size);
|
||||
}
|
||||
if (intersections) { break; }
|
||||
}
|
||||
iterations++;
|
||||
}
|
||||
|
||||
foreach (var labelData in labels)
|
||||
{
|
||||
labelData.OverlapPreventionDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetMouseHoveredLabelIndex(Camera cam)
|
||||
{
|
||||
for (int i = 0; i < labels.Count; i++)
|
||||
{
|
||||
var labelData = labels[i];
|
||||
var drawRect = labelData.GetScreenDrawRect(cam);
|
||||
if (drawRect.Contains(PlayerInput.MousePosition))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static bool RefreshSettings()
|
||||
{
|
||||
bool settingsChanged = false;
|
||||
|
||||
if (GameSettings.CurrentConfig.InteractionLabelDisplayMode != displayMode)
|
||||
{
|
||||
displayMode = GameSettings.CurrentConfig.InteractionLabelDisplayMode;
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
if (GameMain.GraphicsWidth != graphicsWidth || GameMain.GraphicsHeight != graphicsHeight)
|
||||
{
|
||||
graphicsWidth = GameMain.GraphicsWidth;
|
||||
graphicsHeight = GameMain.GraphicsHeight;
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
return settingsChanged;
|
||||
}
|
||||
|
||||
internal static void Update(Character character, Camera cam)
|
||||
{
|
||||
if (RefreshSettings()) { shouldRecalculate = true; recalculateEverything = true; }
|
||||
|
||||
if (shouldRecalculate)
|
||||
{
|
||||
RecalculateLabelPositions(cam, character);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void DrawLabels(SpriteBatch spriteBatch, Camera cam, Character character)
|
||||
{
|
||||
//if any item changes subs or moves significantly, we need to recalculate the label position
|
||||
foreach (var label in labels)
|
||||
{
|
||||
const float MoveThreshold = 150.0f;
|
||||
if (Vector2.DistanceSquared(label.OriginalItemPosition, label.Item.Position) > MoveThreshold * MoveThreshold)
|
||||
{
|
||||
label.TextRect = GetLabelRect(label.Item, cam);
|
||||
}
|
||||
}
|
||||
|
||||
// find out if mouse is on top of any of the labels
|
||||
int mouseOnLabelIndex = GetMouseHoveredLabelIndex(cam);
|
||||
bool isMouseOnLabel = mouseOnLabelIndex >= 0;
|
||||
|
||||
const float LineAlpha = 0.5f;
|
||||
|
||||
if (!isMouseOnLabel)
|
||||
{
|
||||
HoveredItem = null;
|
||||
}
|
||||
|
||||
// draw order: draw lines for labels first
|
||||
for (int i = 0; i < labels.Count; i++)
|
||||
{
|
||||
// Skip the box that the mouse is on, it will be drawn last
|
||||
if (i == mouseOnLabelIndex) { continue; }
|
||||
|
||||
DrawLineForLabel(spriteBatch, cam, labels[i], GUIStyle.InteractionLabelColor * LineAlpha);
|
||||
}
|
||||
|
||||
// Then draw labels
|
||||
for (int i = 0; i < labels.Count; i++)
|
||||
{
|
||||
// Skip the box that the mouse is on, it will be drawn last
|
||||
if (i == mouseOnLabelIndex) { continue; }
|
||||
|
||||
DrawLabelForItem(spriteBatch, cam, labels[i], GUIStyle.InteractionLabelColor);
|
||||
}
|
||||
|
||||
// Draw the label and line that the mouse is on last (for draw order)
|
||||
if (isMouseOnLabel)
|
||||
{
|
||||
var labelData = labels[mouseOnLabelIndex];
|
||||
|
||||
HoveredItem = labelData.Item;
|
||||
|
||||
DrawLineForLabel(spriteBatch, cam, labelData, GUIStyle.InteractionLabelHoverColor * LineAlpha);
|
||||
DrawLabelForItem(spriteBatch, cam,labelData, GUIStyle.InteractionLabelHoverColor);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawLineForLabel(SpriteBatch spriteBatch, Camera cam, LabelData labelData, Color color)
|
||||
{
|
||||
var drawRect = labelData.GetScreenDrawRect(cam);
|
||||
// deflate by one pixel to avoid gap between line and box graphic edge
|
||||
const int lineAnchorInsetPx = 1;
|
||||
var deflateAmount = lineAnchorInsetPx * GUI.Scale;
|
||||
deflateAmount = MathHelper.Max(deflateAmount * Screen.Selected.Cam.Zoom, lineAnchorInsetPx);
|
||||
drawRect.Inflate(-deflateAmount, -deflateAmount);
|
||||
|
||||
var itemDrawPosScreen = labelData.GetInteractableDrawPositionScreen();
|
||||
|
||||
// if item position is inside the box, don't draw a line
|
||||
if (drawRect.Contains(itemDrawPosScreen)) { return; }
|
||||
|
||||
// find the point on the box edge that is closest to the item
|
||||
Vector2 textLineAnchorScreenPos = new Vector2(
|
||||
MathHelper.Clamp(itemDrawPosScreen.X, drawRect.Left, drawRect.Right),
|
||||
MathHelper.Clamp(itemDrawPosScreen.Y, drawRect.Top, drawRect.Bottom));
|
||||
|
||||
// draw line from label to item in the world
|
||||
GUI.DrawLine(spriteBatch, textLineAnchorScreenPos, itemDrawPosScreen, color, depth: 0f, width: 2f);
|
||||
}
|
||||
|
||||
private static void DrawLabelForItem(SpriteBatch spriteBatch, Camera cam, LabelData labelData, Color color)
|
||||
{
|
||||
float scale = Screen.Selected.Cam.Zoom * LabelScale;
|
||||
|
||||
var textDrawRect = labelData.GetScreenDrawRect(cam);
|
||||
RectangleF backgroundRect = textDrawRect;
|
||||
|
||||
// remove margin from the box the text is drawn in
|
||||
textDrawRect.Inflate(-TextBoxMarginPx * scale, -TextBoxMarginPx * scale);
|
||||
Vector2 textDrawPosScreen = new Vector2(textDrawRect.X, textDrawRect.Y);
|
||||
|
||||
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,
|
||||
forceUpperCase: ForceUpperCase.No);
|
||||
}
|
||||
}
|
||||
@@ -483,9 +483,11 @@ namespace Barotrauma
|
||||
//2. check if the base prefab defines the texture
|
||||
if (texturePath.IsNullOrEmpty() && !character.Prefab.VariantOf.IsEmpty)
|
||||
{
|
||||
Identifier speciesName = character.GetBaseCharacterSpeciesName();
|
||||
RagdollParams parentRagdollParams = character.IsHumanoid ?
|
||||
RagdollParams.GetRagdollParams<HumanRagdollParams>(character.Prefab.VariantOf) :
|
||||
RagdollParams.GetRagdollParams<FishRagdollParams>(character.Prefab.VariantOf);
|
||||
RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(speciesName, character.Params, character.Prefab.ContentPackage) :
|
||||
RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(speciesName, character.Params, character.Prefab.ContentPackage);
|
||||
|
||||
texturePath = parentRagdollParams.OriginalElement?.GetAttributeContentPath("texture");
|
||||
}
|
||||
//3. "default case", get the texture from this character's XML
|
||||
@@ -514,8 +516,8 @@ namespace Barotrauma
|
||||
if (characterInfo != null)
|
||||
{
|
||||
spritePath = characterInfo.ReplaceVars(spritePath);
|
||||
|
||||
if (characterInfo.HeadSprite != null && characterInfo.SpriteTags.Any())
|
||||
characterInfo.VerifySpriteTagsLoaded();
|
||||
if (characterInfo.SpriteTags.Any())
|
||||
{
|
||||
string tags = "";
|
||||
characterInfo.SpriteTags.ForEach(tag => tags += $"[{tag}]");
|
||||
@@ -695,7 +697,7 @@ namespace Barotrauma
|
||||
public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null, bool disableDeformations = false)
|
||||
{
|
||||
var spriteParams = Params.GetSprite();
|
||||
if (spriteParams == null) { return; }
|
||||
if (spriteParams == null || Alpha <= 0) { return; }
|
||||
float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength;
|
||||
float brightness = Math.Max(1.0f - burn, 0.2f);
|
||||
Color clr = spriteParams.Color;
|
||||
@@ -724,6 +726,8 @@ namespace Barotrauma
|
||||
|
||||
color = overrideColor ?? color;
|
||||
blankColor = overrideColor ?? blankColor;
|
||||
color *= Alpha;
|
||||
blankColor *= Alpha;
|
||||
|
||||
if (isSevered)
|
||||
{
|
||||
@@ -779,7 +783,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
bool useTintMask = TintMask != null && spriteBatch.GetCurrentEffect() is null;
|
||||
if (useTintMask)
|
||||
if (useTintMask && Sprite?.Texture != null && TintMask?.Texture != null)
|
||||
{
|
||||
tintEffectParams.Effect ??= GameMain.GameScreen.ThresholdTintEffect;
|
||||
tintEffectParams.Params ??= new Dictionary<string, object>();
|
||||
|
||||
Reference in New Issue
Block a user