using Barotrauma.Extensions; using Barotrauma.Networking; using System; using System.Linq; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Xml.Linq; using Barotrauma.IO; using Barotrauma.Items.Components; namespace Barotrauma { partial class CharacterInfo { private static Sprite infoAreaPortraitBG; public bool LastControlled; private Sprite disguisedPortrait; private List disguisedAttachmentSprites; private Vector2? disguisedSheetIndex; private Sprite disguisedJobIcon; private Color disguisedJobColor; public static void Init() { infoAreaPortraitBG = GUI.Style.GetComponentStyle("InfoAreaPortraitBG")?.GetDefaultSprite(); new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(833, 298, 142, 98), null, 0); } public GUIComponent CreateInfoFrame(GUIFrame frame, bool returnParent, Sprite permissionIcon = null) { var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.874f, 0.58f), frame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }) { RelativeSpacing = 0.05f //Stretch = true }; 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) => DrawInfoFrameCharacterIcon(sb, component.Rect)); ScalableFont font = paddedFrame.Rect.Width < 280 ? GUI.SmallFont : GUI.Font; var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform)) { RelativeSpacing = 0.02f, Stretch = true }; Color? nameColor = null; if (Job != null) { nameColor = Job.Prefab.UIColor; } GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUI.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUI.Font) { ForceUpperCase = true, Padding = Vector4.Zero }; if (permissionIcon != null) { Point iconSize = permissionIcon.SourceRect.Size; int iconWidth = (int)((float)characterNameBlock.Rect.Height / iconSize.Y * iconSize.X); new GUIImage(new RectTransform(new Point(iconWidth, characterNameBlock.Rect.Height), characterNameBlock.RectTransform) { AbsoluteOffset = new Point(-iconWidth - 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true }; } if (Job != null) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), Job.Name, textColor: Job.Prefab.UIColor, font: font) { Padding = Vector4.Zero }; } if (personalityTrait != null) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + personalityTrait.Name.Replace(" ", ""))), font: font) { Padding = Vector4.Zero }; } if (Job != null && (Character == null || !Character.IsDead)) { var skillsArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) { Stretch = true }; var skills = Job.Skills; skills.Sort((s1, s2) => -s1.Level.CompareTo(s2.Level)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("skills"), string.Empty), font: font) { Padding = Vector4.Zero }; foreach (Skill skill in skills) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); var skillName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.Get("SkillName." + skill.Identifier), textColor: textColor, font: font) { Padding = Vector4.Zero }; float modifiedSkillLevel = skill.Level; if (Character != null) { modifiedSkillLevel = Character.GetSkillLevel(skill.Identifier); } if (!MathUtils.NearlyEqual(MathF.Round(modifiedSkillLevel), MathF.Round(skill.Level))) { int skillChange = (int)MathF.Round(modifiedSkillLevel - skill.Level); string changeText = $"{(skillChange > 0 ? "+" : "") + skillChange}"; new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), skillName.RectTransform), $"{(int)skill.Level} ({changeText})", textColor: textColor, font: font, textAlignment: Alignment.CenterRight); } else { new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), skillName.RectTransform), ((int)skill.Level).ToString(), textColor: textColor, font: font, textAlignment: Alignment.CenterRight); } } } else if (Character != null && Character.IsDead) { var deadArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) { Stretch = true }; string deadDescription = TextManager.AddPunctuation(':', TextManager.Get("deceased") + "\n" + Character.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? TextManager.AddPunctuation(':', TextManager.Get("CauseOfDeath"), TextManager.Get("CauseOfDeath." + Character.CauseOfDeath.Type.ToString()))); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), deadArea.RectTransform), deadDescription, textColor: GUI.Style.Red, font: font, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; } if (returnParent) { return frame; } else { return paddedFrame; } } private void DrawInfoFrameCharacterIcon(SpriteBatch sb, Rectangle componentRect) { 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); } public GUIFrame CreateCharacterFrame(GUIComponent parent, string text, object userData) { GUIFrame frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, 40), parent.RectTransform) { IsFixedSize = false }, "ListBoxElement") { UserData = userData }; Color? textColor = null; if (Job != null) { textColor = Job.Prefab.UIColor; } GUITextBlock textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(40, 0) }, text, textColor: textColor, font: GUI.SmallFont); new GUICustomComponent(new RectTransform(new Point(frame.Rect.Height, frame.Rect.Height), frame.RectTransform, Anchor.CenterLeft) { IsFixedSize = false }, onDraw: (sb, component) => DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); return frame; } partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos) { if (TeamID == CharacterTeamType.FriendlyNPC) { return; } if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } if ((int)newLevel > (int)prevLevel) { int increase = Math.Max((int)newLevel - (int)prevLevel, 1); GUI.AddMessage( string.Format("+{0} {1}", increase, TextManager.Get("SkillName." + skillIdentifier)), GUI.Style.Green, textPopupPos, Vector2.UnitY * 10.0f, playSound: false, subId: Character?.Submarine?.ID ?? -1); } } private void GetDisguisedSprites(IdCard idCard) { if (idCard.Item.Tags == string.Empty) return; if (idCard.StoredJobPrefab == null || idCard.StoredPortrait == null) { string[] readTags = idCard.Item.Tags.Split(','); if (readTags.Length == 0) return; if (idCard.StoredJobPrefab == null) { string jobIdTag = readTags.FirstOrDefault(s => s.StartsWith("jobid:")); if (jobIdTag != null && jobIdTag.Length > 6) { string jobId = jobIdTag.Substring(6); if (jobId != string.Empty) { idCard.StoredJobPrefab = JobPrefab.Get(jobId); } } } if (idCard.StoredPortrait == null) { string disguisedGender = string.Empty; string disguisedRace = string.Empty; string disguisedHeadSpriteId = string.Empty; int disguisedHairIndex = -1; int disguisedBeardIndex = -1; int disguisedMoustacheIndex = -1; int disguisedFaceAttachmentIndex = -1; foreach (string tag in readTags) { string[] s = tag.Split(':'); switch (s[0]) { case "gender": disguisedGender = s[1]; break; case "race": disguisedRace = s[1]; break; case "headspriteid": disguisedHeadSpriteId = s[1]; break; case "hairindex": disguisedHairIndex = int.Parse(s[1]); break; case "beardindex": disguisedBeardIndex = int.Parse(s[1]); break; case "moustacheindex": disguisedMoustacheIndex = int.Parse(s[1]); break; case "faceattachmentindex": disguisedFaceAttachmentIndex = int.Parse(s[1]); break; case "sheetindex": string[] vectorValues = s[1].Split(";"); idCard.StoredSheetIndex = new Vector2(float.Parse(vectorValues[0]), float.Parse(vectorValues[1])); break; } } if (disguisedGender == string.Empty || disguisedRace == string.Empty || disguisedHeadSpriteId == string.Empty) { idCard.StoredPortrait = null; idCard.StoredAttachments = null; return; } foreach (XElement limbElement in Ragdoll.MainElement.Elements()) { if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } XElement spriteElement = limbElement.Element("sprite"); if (spriteElement == null) { continue; } string spritePath = spriteElement.Attribute("texture").Value; spritePath = spritePath.Replace("[GENDER]", disguisedGender); spritePath = spritePath.Replace("[RACE]", disguisedRace.ToLowerInvariant()); spritePath = spritePath.Replace("[HEADID]", disguisedHeadSpriteId); string fileName = Path.GetFileNameWithoutExtension(spritePath); //go through the files in the directory to find a matching sprite foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) { if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { continue; } string fileWithoutTags = Path.GetFileNameWithoutExtension(file); fileWithoutTags = fileWithoutTags.Split('[', ']').First(); if (fileWithoutTags != fileName) { continue; } idCard.StoredPortrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; break; } break; } if (Wearables != null) { XElement disguisedHairElement, disguisedBeardElement, disguisedMoustacheElement, disguisedFaceAttachmentElement; List disguisedHairs, disguisedBeards, disguisedMoustaches, disguisedFaceAttachments; Gender disguisedGenderEnum = disguisedGender == "female" ? Gender.Female : Gender.Male; Race disguisedRaceEnum = (Race)Enum.Parse(typeof(Race), disguisedRace); int headSpriteId = int.Parse(disguisedHeadSpriteId); float commonness = disguisedGenderEnum == Gender.Female ? 0.05f : 0.2f; disguisedHairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.Hair, headSpriteId), WearableType.Hair, commonness); disguisedBeards = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.Beard, headSpriteId), WearableType.Beard); disguisedMoustaches = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.Moustache, headSpriteId), WearableType.Moustache); disguisedFaceAttachments = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.FaceAttachment, headSpriteId), WearableType.FaceAttachment); if (IsValidIndex(disguisedHairIndex, disguisedHairs)) { disguisedHairElement = disguisedHairs[disguisedHairIndex]; } else { disguisedHairElement = GetRandomElement(disguisedHairs); } if (IsValidIndex(disguisedBeardIndex, disguisedBeards)) { disguisedBeardElement = disguisedBeards[disguisedBeardIndex]; } else { disguisedBeardElement = GetRandomElement(disguisedBeards); } if (IsValidIndex(disguisedMoustacheIndex, disguisedMoustaches)) { disguisedMoustacheElement = disguisedMoustaches[disguisedMoustacheIndex]; } else { disguisedMoustacheElement = GetRandomElement(disguisedMoustaches); } if (IsValidIndex(disguisedFaceAttachmentIndex, disguisedFaceAttachments)) { disguisedFaceAttachmentElement = disguisedFaceAttachments[disguisedFaceAttachmentIndex]; } else { disguisedFaceAttachmentElement = GetRandomElement(disguisedFaceAttachments); } idCard.StoredAttachments = new List(); disguisedFaceAttachmentElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.FaceAttachment))); disguisedBeardElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.Beard))); disguisedMoustacheElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.Moustache))); disguisedHairElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.Hair))); if (OmitJobInPortraitClothing) { JobPrefab.NoJobElement?.Element("PortraitClothing")?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.JobIndicator))); } else { idCard.StoredJobPrefab?.ClothingElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.JobIndicator))); } } } } if (idCard.StoredJobPrefab != null) { disguisedJobIcon = idCard.StoredJobPrefab.Icon; disguisedJobColor = idCard.StoredJobPrefab.UIColor; } disguisedPortrait = idCard.StoredPortrait; disguisedSheetIndex = idCard.StoredSheetIndex; disguisedAttachmentSprites = idCard.StoredAttachments; } partial void LoadAttachmentSprites(bool omitJob) { if (attachmentSprites == null) { attachmentSprites = new List(); } if (!IsAttachmentsLoaded) { LoadHeadAttachments(); } FaceAttachment?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.FaceAttachment))); BeardElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Beard))); MoustacheElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Moustache))); HairElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Hair))); if (omitJob) { JobPrefab.NoJobElement?.Element("PortraitClothing")?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); } else { Job?.Prefab.ClothingElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); } } // Doesn't work if the head's source rect does not start at 0,0. public static Point CalculateOffset(Sprite sprite, Point offset) => sprite.SourceRect.Size * offset; public void CalculateHeadPosition(Sprite sprite) { if (sprite == null) { return; } if (Head.SheetIndex == null) { return; } Point location = CalculateOffset(sprite, Head.SheetIndex.Value.ToPoint()); sprite.SourceRect = new Rectangle(location, sprite.SourceRect.Size); } public void DrawBackground(SpriteBatch spriteBatch) { if (infoAreaPortraitBG == null) { return; } infoAreaPortraitBG.Draw(spriteBatch, HUDLayoutSettings.BottomRightInfoArea.Location.ToVector2(), Color.White, Vector2.Zero, 0.0f, scale: new Vector2( HUDLayoutSettings.BottomRightInfoArea.Width / (float)infoAreaPortraitBG.SourceRect.Width, HUDLayoutSettings.BottomRightInfoArea.Height / (float)infoAreaPortraitBG.SourceRect.Height)); } public void DrawPortrait(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 offset, float targetWidth, bool flip = false, bool evaluateDisguise = false) { if (evaluateDisguise && IsDisguised) return; Vector2? sheetIndex; Sprite portraitToDraw; List attachmentsToDraw; if (!IsDisguisedAsAnother || !evaluateDisguise) { sheetIndex = Head.SheetIndex; portraitToDraw = Portrait; attachmentsToDraw = AttachmentSprites; } else { sheetIndex = disguisedSheetIndex; portraitToDraw = disguisedPortrait; attachmentsToDraw = disguisedAttachmentSprites; } if (portraitToDraw != null) { // Scale down the head sprite 10% float scale = targetWidth * 0.9f / Portrait.size.X; if (sheetIndex.HasValue) { portraitToDraw.SourceRect = new Rectangle(CalculateOffset(portraitToDraw, sheetIndex.Value.ToPoint()), portraitToDraw.SourceRect.Size); } portraitToDraw.Draw(spriteBatch, screenPos + offset, Color.White, portraitToDraw.Origin, scale: scale, spriteEffect: flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); if (attachmentsToDraw != null) { float depthStep = 0.000001f; foreach (var attachment in attachmentsToDraw) { DrawAttachmentSprite(spriteBatch, attachment, portraitToDraw, sheetIndex, screenPos + offset, scale, depthStep, flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); depthStep += depthStep; } } } } public void DrawIcon(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 targetAreaSize) { var headSprite = HeadSprite; if (headSprite != null) { float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); if (Head.SheetIndex.HasValue) { headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.Value.ToPoint()), headSprite.SourceRect.Size); } headSprite.Draw(spriteBatch, screenPos, scale: scale); if (AttachmentSprites != null) { float depthStep = 0.000001f; foreach (var attachment in AttachmentSprites) { DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep); depthStep += depthStep; } } } } public void DrawJobIcon(SpriteBatch spriteBatch, Vector2 pos, float scale = 1.0f, bool evaluateDisguise = false) { if (evaluateDisguise && IsDisguised) return; var icon = !IsDisguisedAsAnother || !evaluateDisguise ? Job?.Prefab?.Icon : disguisedJobIcon; if (icon == null) { return; } Color iconColor = !IsDisguisedAsAnother || !evaluateDisguise ? Job.Prefab.UIColor : disguisedJobColor; icon.Draw(spriteBatch, pos, iconColor, scale: scale); } public void DrawJobIcon(SpriteBatch spriteBatch, Rectangle area, bool evaluateDisguise = false) { if (evaluateDisguise && IsDisguised) return; var icon = !IsDisguisedAsAnother || !evaluateDisguise ? Job?.Prefab?.Icon : disguisedJobIcon; if (icon == null) { return; } Color iconColor = !IsDisguisedAsAnother || !evaluateDisguise ? Job.Prefab.UIColor : disguisedJobColor; icon.Draw(spriteBatch, area.Center.ToVector2(), iconColor, scale: Math.Min(area.Width / (float)icon.SourceRect.Width, area.Height / (float)icon.SourceRect.Height)); } private void DrawAttachmentSprite(SpriteBatch spriteBatch, WearableSprite attachment, Sprite head, Vector2? sheetIndex, Vector2 drawPos, float scale, float depthStep, SpriteEffects spriteEffects = SpriteEffects.None) { if (attachment.InheritSourceRect) { if (attachment.SheetIndex.HasValue) { attachment.Sprite.SourceRect = new Rectangle(CalculateOffset(head, attachment.SheetIndex.Value), head.SourceRect.Size); } else if (sheetIndex.HasValue) { attachment.Sprite.SourceRect = new Rectangle(CalculateOffset(head, sheetIndex.Value.ToPoint()), head.SourceRect.Size); } else { attachment.Sprite.SourceRect = head.SourceRect; } } Vector2 origin = attachment.Sprite.Origin; if (attachment.InheritOrigin) { origin = head.Origin; attachment.Sprite.Origin = origin; } else { origin = attachment.Sprite.Origin; } float depth = attachment.Sprite.Depth; if (attachment.InheritLimbDepth) { depth = head.Depth - depthStep; } attachment.Sprite.Draw(spriteBatch, drawPos, Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); } public static CharacterInfo ClientRead(string speciesName, IReadMessage inc) { ushort infoID = inc.ReadUInt16(); string newName = inc.ReadString(); string originalName = inc.ReadString(); int gender = inc.ReadByte(); int race = inc.ReadByte(); int headSpriteID = inc.ReadByte(); int hairIndex = inc.ReadByte(); int beardIndex = inc.ReadByte(); int moustacheIndex = inc.ReadByte(); int faceAttachmentIndex = inc.ReadByte(); string ragdollFile = inc.ReadString(); string jobIdentifier = inc.ReadString(); int variant = inc.ReadByte(); JobPrefab jobPrefab = null; Dictionary skillLevels = new Dictionary(); if (!string.IsNullOrEmpty(jobIdentifier)) { jobPrefab = JobPrefab.Get(jobIdentifier); byte skillCount = inc.ReadByte(); for (int i = 0; i < skillCount; i++) { string skillIdentifier = inc.ReadString(); float skillLevel = inc.ReadSingle(); skillLevels.Add(skillIdentifier, skillLevel); } } // TODO: animations CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant) { ID = infoID, }; ch.RecreateHead(headSpriteID,(Race)race, (Gender)gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); if (ch.Job != null) { foreach (KeyValuePair skill in skillLevels) { Skill matchingSkill = ch.Job.Skills.Find(s => s.Identifier == skill.Key); if (matchingSkill == null) { ch.Job.Skills.Add(new Skill(skill.Key, skill.Value)); continue; } matchingSkill.Level = skill.Value; } ch.Job.Skills.RemoveAll(s => !skillLevels.ContainsKey(s.Identifier)); } return ch; } } }