Files
ai-images/Source/AIImages/Window_AIImage.cs

514 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.Linq;
using System.Text;
using RimWorld;
using UnityEngine;
using Verse;
#pragma warning disable IDE1006 // Naming Styles
namespace AIImages
{
/// <summary>
/// Empty window that opens when clicking the pawn button
/// </summary>
[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;
/// <summary>
/// Обновляет текущую пешку в окне
/// </summary>
public void UpdatePawn(Pawn newPawn)
{
this.pawn = newPawn;
}
/// <summary>
/// Получить текущую пешку
/// </summary>
public Pawn CurrentPawn => pawn;
/// <summary>
/// Вызывается каждый кадр для обновления окна
/// </summary>
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;
}
}
/// <summary>
/// Получает описание внешности персонажа
/// </summary>
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();
}
/// <summary>
/// Получает описание одежды персонажа
/// </summary>
private string GetApparelDescription()
{
if (pawn?.apparel == null)
return "AIImages.Apparel.NoInfo".Translate();
StringBuilder sb = new StringBuilder();
List<Apparel> 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();
}
/// <summary>
/// Форматирует информацию об одном предмете одежды
/// </summary>
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();
}
/// <summary>
/// Генерирует промпт для Stable Diffusion на основе внешности персонажа
/// </summary>
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<string> 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)}, ";
}
/// <summary>
/// Получает текстовое описание цвета
/// </summary>
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();
}
/// <summary>
/// Вычисляет высоту всего контента для прокрутки
/// </summary>
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;
}
}
}