using System.Collections.Generic; using System.Linq; using System.Text; using RimWorld; using UnityEngine; using Verse; #pragma warning disable IDE1006 // Naming Styles namespace AIImages { /// /// Empty window that opens when clicking the pawn button /// [System.Diagnostics.CodeAnalysis.SuppressMessage( "Style", "IDE1006:Naming Styles", Justification = "RimWorld Window naming convention" )] [System.Diagnostics.CodeAnalysis.SuppressMessage( "Minor Code Smell", "S101:Types should be named in PascalCase", Justification = "RimWorld Window naming convention" )] public class Window_AIImage : Window { private Pawn pawn; public Window_AIImage(Pawn pawn) { this.pawn = pawn; this.doCloseX = true; this.doCloseButton = true; this.forcePause = false; // Не ставим игру на паузу this.absorbInputAroundWindow = false; // Не блокируем клики вне окна this.draggable = true; // Делаем окно перемещаемым this.preventCameraMotion = false; // Не блокируем управление камерой } public override Vector2 InitialSize => new Vector2(700f, 700f); private Vector2 scrollPosition = Vector2.zero; private float copiedMessageTime = 0f; /// /// Обновляет текущую пешку в окне /// public void UpdatePawn(Pawn newPawn) { this.pawn = newPawn; } /// /// Получить текущую пешку /// public Pawn CurrentPawn => pawn; /// /// Вызывается каждый кадр для обновления окна /// public override void WindowUpdate() { base.WindowUpdate(); // Проверяем, изменилась ли выбранная пешка Pawn selectedPawn = Find.Selector.SelectedPawns.FirstOrDefault(p => p.IsColonist && p.Spawned && p.Faction == Faction.OfPlayer ); // Если выбрана новая колонистская пешка, обновляем окно if (selectedPawn != null && selectedPawn != pawn) { pawn = selectedPawn; } // Уменьшаем таймер сообщения о копировании if (copiedMessageTime > 0f) { copiedMessageTime -= Time.deltaTime; } } /// /// Получает описание внешности персонажа /// private string GetAppearanceDescription() { if (pawn?.story == null) return "AIImages.Appearance.NoInfo".Translate(); StringBuilder sb = new StringBuilder(); // Пол sb.AppendLine("AIImages.Appearance.Gender".Translate(pawn.gender.GetLabel())); // Возраст sb.AppendLine("AIImages.Appearance.Age".Translate(pawn.ageTracker.AgeBiologicalYears)); // Тип тела if (pawn.story.bodyType != null) { sb.AppendLine( "AIImages.Appearance.BodyType".Translate(pawn.story.bodyType.defName) ); } // Цвет кожи if (pawn.story.SkinColor != null) { Color skinColor = pawn.story.SkinColor; sb.AppendLine( "AIImages.Appearance.SkinColor".Translate( skinColor.r.ToString("F2"), skinColor.g.ToString("F2"), skinColor.b.ToString("F2") ) ); } // Волосы if (pawn.story.hairDef != null) { sb.AppendLine("AIImages.Appearance.Hairstyle".Translate(pawn.story.hairDef.label)); if (pawn.story.HairColor != null) { sb.AppendLine( "AIImages.Appearance.HairColor".Translate( pawn.story.HairColor.r.ToString("F2"), pawn.story.HairColor.g.ToString("F2"), pawn.story.HairColor.b.ToString("F2") ) ); } } // Черты характера if (pawn.story.traits?.allTraits != null && pawn.story.traits.allTraits.Any()) { sb.AppendLine("\n" + "AIImages.Appearance.Traits".Translate()); foreach (var trait in pawn.story.traits.allTraits) { sb.AppendLine($" • {trait.LabelCap}"); } } return sb.ToString(); } /// /// Получает описание одежды персонажа /// private string GetApparelDescription() { if (pawn?.apparel == null) return "AIImages.Apparel.NoInfo".Translate(); StringBuilder sb = new StringBuilder(); List wornApparel = pawn.apparel.WornApparel; if (wornApparel == null || !wornApparel.Any()) { sb.AppendLine("AIImages.Apparel.NoClothes".Translate()); } else { sb.AppendLine("AIImages.Apparel.ListHeader".Translate(wornApparel.Count) + "\n"); foreach (Apparel apparel in wornApparel) { FormatApparelItem(sb, apparel); } } return sb.ToString(); } /// /// Форматирует информацию об одном предмете одежды /// private void FormatApparelItem(StringBuilder sb, Apparel apparel) { sb.AppendLine($"• {apparel.LabelCap}"); if (apparel.TryGetQuality(out QualityCategory quality)) { sb.AppendLine("AIImages.Apparel.Quality".Translate(quality.GetLabel())); } if (apparel.Stuff != null) { sb.AppendLine("AIImages.Apparel.Material".Translate(apparel.Stuff.LabelCap)); } if (apparel.HitPoints < apparel.MaxHitPoints) { int percentage = (int)((float)apparel.HitPoints / apparel.MaxHitPoints * 100); sb.AppendLine( "AIImages.Apparel.Durability".Translate( apparel.HitPoints, apparel.MaxHitPoints, percentage ) ); } if (apparel.DrawColor != Color.white) { sb.AppendLine( "AIImages.Apparel.Color".Translate( apparel.DrawColor.r.ToString("F2"), apparel.DrawColor.g.ToString("F2"), apparel.DrawColor.b.ToString("F2") ) ); } sb.AppendLine(); } /// /// Генерирует промпт для Stable Diffusion на основе внешности персонажа /// private string GenerateStableDiffusionPrompt() { if (pawn?.story == null) return "portrait of a person"; StringBuilder prompt = new StringBuilder("portrait of a "); prompt.Append(GetAgeAndGenderDescription()); prompt.Append(GetBodyTypeDescription()); prompt.Append(GetSkinToneDescription()); prompt.Append(GetHairDescription()); prompt.Append(GetApparelPromptDescription()); prompt.Append( "realistic, detailed, high quality, professional lighting, 8k, photorealistic" ); return prompt.ToString(); } private string GetAgeAndGenderDescription() { string ageGroup = pawn.ageTracker.AgeBiologicalYears switch { < 18 => "young", < 30 => "young adult", < 50 => "middle-aged", _ => "mature", }; return $"{ageGroup} {pawn.gender.GetLabel()}, "; } private string GetBodyTypeDescription() { if (pawn.story.bodyType == null) return ""; string bodyDesc = pawn.story.bodyType.defName.ToLower() switch { "thin" => "slender build", "hulk" => "muscular build", "fat" => "heavyset build", _ => "average build", }; return $"{bodyDesc}, "; } private string GetSkinToneDescription() { if (pawn.story.SkinColor == null) return ""; float brightness = (pawn.story.SkinColor.r + pawn.story.SkinColor.g + pawn.story.SkinColor.b) / 3f; string skinTone = brightness switch { >= 0.8f => "fair skin", >= 0.6f => "light skin", >= 0.4f => "olive skin", >= 0.2f => "brown skin", _ => "dark skin", }; return $"{skinTone}, "; } private string GetHairDescription() { if (pawn.story.hairDef == null) return ""; string result = $"{pawn.story.hairDef.label.ToLower()} hair"; if (pawn.story.HairColor != null) { result += $", {GetColorDescription(pawn.story.HairColor)} hair color"; } return result + ", "; } private string GetApparelPromptDescription() { if (pawn.apparel?.WornApparel == null || !pawn.apparel.WornApparel.Any()) return ""; List items = pawn .apparel.WornApparel.Take(3) .Select(a => a.Stuff != null ? $"{a.Stuff.label.ToLower()} {a.def.label.ToLower()}" : a.def.label.ToLower() ) .ToList(); return $"wearing {string.Join(", ", items)}, "; } /// /// Получает текстовое описание цвета /// private string GetColorDescription(Color color) { // Определяем доминирующий цвет float max = Mathf.Max(color.r, color.g, color.b); float min = Mathf.Min(color.r, color.g, color.b); float diff = max - min; if (diff < 0.1f) { // Оттенки серого return max switch { > 0.8f => "white", > 0.6f => "light gray", > 0.4f => "gray", > 0.2f => "dark gray", _ => "black", }; } // Цветные const float epsilon = 0.001f; if (Mathf.Abs(color.r - max) < epsilon) { return color.g > color.b ? "orange" : "red"; } else if (Mathf.Abs(color.g - max) < epsilon) { return color.r > color.b ? "yellow" : "green"; } else { return color.r > color.g ? "purple" : "blue"; } } public override void DoWindowContents(Rect inRect) { float curY = 0f; // Заголовок Text.Font = GameFont.Medium; Widgets.Label( new Rect(0f, curY, inRect.width, 40f), "AIImages.Window.Title".Translate() ); curY += 45f; // Имя пешки Text.Font = GameFont.Small; Widgets.Label( new Rect(0f, curY, inRect.width, 30f), "AIImages.Window.PawnLabel".Translate(pawn.NameShortColored.Resolve()) ); curY += 40f; // Разделитель Widgets.DrawLineHorizontal(0f, curY, inRect.width); curY += 10f; // Область для прокрутки контента Rect scrollRect = new Rect(0f, curY, inRect.width, inRect.height - curY); Rect scrollViewRect = new Rect( 0f, 0f, scrollRect.width - 20f, CalculateContentHeight() ); Widgets.BeginScrollView(scrollRect, ref scrollPosition, scrollViewRect); float contentY = 0f; // Секция "Внешность" Text.Font = GameFont.Medium; Widgets.Label( new Rect(10f, contentY, scrollViewRect.width - 20f, 30f), "AIImages.Appearance.SectionTitle".Translate() ); contentY += 35f; Text.Font = GameFont.Small; string appearanceText = GetAppearanceDescription(); float appearanceHeight = Text.CalcHeight(appearanceText, scrollViewRect.width - 30f); Widgets.Label( new Rect(20f, contentY, scrollViewRect.width - 30f, appearanceHeight), appearanceText ); contentY += appearanceHeight + 20f; // Разделитель Widgets.DrawLineHorizontal(10f, contentY, scrollViewRect.width - 20f); contentY += 15f; // Секция "Одежда" Text.Font = GameFont.Medium; Widgets.Label( new Rect(10f, contentY, scrollViewRect.width - 20f, 30f), "AIImages.Apparel.SectionTitle".Translate() ); contentY += 35f; Text.Font = GameFont.Small; string apparelText = GetApparelDescription(); float apparelHeight = Text.CalcHeight(apparelText, scrollViewRect.width - 30f); Widgets.Label( new Rect(20f, contentY, scrollViewRect.width - 30f, apparelHeight), apparelText ); contentY += apparelHeight + 20f; // Разделитель Widgets.DrawLineHorizontal(10f, contentY, scrollViewRect.width - 20f); contentY += 15f; // Секция "Stable Diffusion Промпт" Text.Font = GameFont.Medium; Widgets.Label( new Rect(10f, contentY, scrollViewRect.width - 20f, 30f), "AIImages.Prompt.SectionTitle".Translate() ); contentY += 35f; // Промпт текст Text.Font = GameFont.Small; string promptText = GenerateStableDiffusionPrompt(); float promptHeight = Text.CalcHeight(promptText, scrollViewRect.width - 30f); Widgets.Label( new Rect(20f, contentY, scrollViewRect.width - 30f, promptHeight), promptText ); contentY += promptHeight + 10f; // Кнопка копирования Rect copyButtonRect = new Rect(20f, contentY, 150f, 30f); if (Widgets.ButtonText(copyButtonRect, "AIImages.Prompt.CopyButton".Translate())) { GUIUtility.systemCopyBuffer = promptText; copiedMessageTime = 2f; // Показываем сообщение на 2 секунды } // Сообщение о копировании if (copiedMessageTime > 0f) { Rect copiedRect = new Rect(copyButtonRect.xMax + 10f, contentY, 100f, 30f); GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f); // Затухающий зеленый Widgets.Label(copiedRect, "AIImages.Prompt.Copied".Translate()); GUI.color = Color.white; } Widgets.EndScrollView(); } /// /// Вычисляет высоту всего контента для прокрутки /// private float CalculateContentHeight() { float height = 0f; // Заголовок "Внешность" height += 35f; // Текст внешности string appearanceText = GetAppearanceDescription(); height += Text.CalcHeight(appearanceText, 640f) + 20f; // Разделитель height += 15f; // Заголовок "Одежда" height += 35f; // Текст одежды string apparelText = GetApparelDescription(); height += Text.CalcHeight(apparelText, 640f) + 20f; // Разделитель height += 15f; // Заголовок "Промпт" height += 35f; // Текст промпта string promptText = GenerateStableDiffusionPrompt(); height += Text.CalcHeight(promptText, 640f) + 10f; // Кнопка и отступ height += 30f + 20f; return height; } } }