514 lines
17 KiB
C#
514 lines
17 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|