diff --git a/Assemblies/AIImages.dll b/Assemblies/AIImages.dll
index 3ae3f55..35453bd 100644
Binary files a/Assemblies/AIImages.dll and b/Assemblies/AIImages.dll differ
diff --git a/Languages/English/Keyed/AIImages.xml b/Languages/English/Keyed/AIImages.xml
index 7091f44..28a0bce 100644
--- a/Languages/English/Keyed/AIImages.xml
+++ b/Languages/English/Keyed/AIImages.xml
@@ -2,19 +2,21 @@
AI Image
- Open AI Image window
+ Open AI Image window to generate character portraits
- AI Image Window
+ AI Image Generator
Character: {0}
+ Refresh
Appearance
Appearance information unavailable
Gender: {0}
Age: {0} years
Body type: {0}
- Skin color: RGB({0}, {1}, {2})
+ Skin tone: {0}
Hairstyle: {0}
- Hair color: RGB({0}, {1}, {2})
+ Hair color: {0}
+ Beard: {0}
Traits:
Apparel
@@ -24,9 +26,45 @@
Quality: {0}
Material: {0}
Durability: {0}/{1} ({2}%)
- Color: RGB({0}, {1}, {2})
+ Color: {0}
Stable Diffusion Prompt
Copy Prompt
Copied!
+
+ Generate Image
+ Generating...
+ Generating image, please wait...
+ Image generated successfully!
+ Generation failed
+ Image saved to: {0}
+ No image generated yet.\nClick "Generate Image" to start.
+
+ API Settings
+ Configure connection to Stable Diffusion API
+ API Endpoint
+ Test Connection
+ Load Available Models
+ Successfully connected to API!
+ Failed to connect to API. Check endpoint and ensure Stable Diffusion WebUI is running.
+ Loaded {0} models from API
+ No models found. Check API connection.
+ Generation Settings
+ Configure image generation parameters
+ Art Style
+ Shot Type
+ Sampling Steps
+ CFG Scale
+ Width
+ Height
+ Sampler
+ Prompts
+ Base prompts that will be added to all generations
+ Base Positive Prompt
+ Base Negative Prompt
+ Options
+ Auto-load models on startup
+ Show technical information
+ Save generation history
+ Save Path
diff --git a/Languages/Russian/Keyed/AIImages.xml b/Languages/Russian/Keyed/AIImages.xml
index dc4375d..fcdd1d7 100644
--- a/Languages/Russian/Keyed/AIImages.xml
+++ b/Languages/Russian/Keyed/AIImages.xml
@@ -1,22 +1,24 @@
-
- AI Изображение
- Открыть окно AI изображения
-
- Окно AI изображения
+
+ AI Портрет
+ Открыть окно генерации AI портретов персонажа
+
+ Генератор AI Изображений
Персонаж: {0}
-
+ Обновить
+
Внешность
Информация о внешности недоступна
Пол: {0}
Возраст: {0} лет
Тип тела: {0}
- Цвет кожи: RGB({0}, {1}, {2})
+ Тон кожи: {0}
Прическа: {0}
- Цвет волос: RGB({0}, {1}, {2})
+ Цвет волос: {0}
+ Борода: {0}
Черты характера:
-
+
Одежда
Информация об одежде недоступна
Персонаж ничего не носит
@@ -24,9 +26,45 @@
Качество: {0}
Материал: {0}
Прочность: {0}/{1} ({2}%)
- Цвет: RGB({0}, {1}, {2})
-
- Промпт для Stable Diffusion
+ Цвет: {0}
+
+ Промпт Stable Diffusion
Копировать промпт
Скопировано!
+
+ Сгенерировать изображение
+ Генерация...
+ Генерируется изображение, пожалуйста подождите...
+ Изображение успешно сгенерировано!
+ Ошибка генерации
+ Изображение сохранено в: {0}
+ Изображение еще не сгенерировано.\nНажмите "Сгенерировать изображение" для начала.
+
+ Настройки API
+ Настройка подключения к API Stable Diffusion
+ Адрес API
+ Проверить соединение
+ Загрузить доступные модели
+ Успешное подключение к API!
+ Не удалось подключиться к API. Проверьте адрес и убедитесь, что Stable Diffusion WebUI запущен.
+ Загружено {0} моделей из API
+ Модели не найдены. Проверьте подключение к API.
+ Настройки генерации
+ Настройка параметров генерации изображений
+ Художественный стиль
+ Тип кадра
+ Количество шагов сэмплирования
+ CFG Scale
+ Ширина
+ Высота
+ Сэмплер
+ Промпты
+ Базовые промпты, которые будут добавлены ко всем генерациям
+ Базовый позитивный промпт
+ Базовый негативный промпт
+ Опции
+ Автоматически загружать модели при запуске
+ Показывать техническую информацию
+ Сохранять историю генераций
+ Путь для сохранения
diff --git a/Source/AIImages/AIImagesMod.cs b/Source/AIImages/AIImagesMod.cs
index 4aac809..8700b50 100644
--- a/Source/AIImages/AIImagesMod.cs
+++ b/Source/AIImages/AIImagesMod.cs
@@ -1,19 +1,59 @@
+using AIImages.Services;
+using AIImages.Settings;
using HarmonyLib;
+using UnityEngine;
using Verse;
namespace AIImages
{
///
- /// Main mod class that initializes Harmony patches
+ /// Main mod class with settings support
+ ///
+ public class AIImagesMod : Mod
+ {
+ public static AIImagesModSettings Settings { get; private set; }
+
+ // Singleton сервисы
+ public static IPawnDescriptionService PawnDescriptionService { get; private set; }
+ public static IPromptGeneratorService PromptGeneratorService { get; private set; }
+ public static IStableDiffusionApiService ApiService { get; private set; }
+
+ public AIImagesMod(ModContentPack content)
+ : base(content)
+ {
+ Settings = GetSettings();
+
+ // Инициализируем сервисы
+ PawnDescriptionService = new PawnDescriptionService();
+ PromptGeneratorService = new AdvancedPromptGenerator();
+ ApiService = new StableDiffusionApiService(Settings.savePath);
+
+ Log.Message("[AI Images] Mod initialized successfully with settings");
+ }
+
+ public override void DoSettingsWindowContents(Rect inRect)
+ {
+ AIImagesSettingsUI.DoSettingsWindowContents(inRect, Settings);
+ base.DoSettingsWindowContents(inRect);
+ }
+
+ public override string SettingsCategory()
+ {
+ return "AI Images";
+ }
+ }
+
+ ///
+ /// Static constructor for Harmony patches
///
[StaticConstructorOnStartup]
- public static class AIImagesMod
+ public static class AIImagesHarmonyPatcher
{
- static AIImagesMod()
+ static AIImagesHarmonyPatcher()
{
var harmony = new Harmony("Mrleo1nid.aiimages");
harmony.PatchAll();
- Log.Message("[AI Images] Mod initialized successfully");
+ Log.Message("[AI Images] Harmony patches applied successfully");
}
}
}
diff --git a/Source/AIImages/Models/GenerationRequest.cs b/Source/AIImages/Models/GenerationRequest.cs
new file mode 100644
index 0000000..3843729
--- /dev/null
+++ b/Source/AIImages/Models/GenerationRequest.cs
@@ -0,0 +1,50 @@
+namespace AIImages.Models
+{
+ ///
+ /// Запрос на генерацию изображения
+ ///
+ public class GenerationRequest
+ {
+ public string Prompt { get; set; }
+ public string NegativePrompt { get; set; }
+ public int Steps { get; set; }
+ public float CfgScale { get; set; }
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public string Sampler { get; set; }
+ public int Seed { get; set; }
+ public string Model { get; set; }
+ }
+
+ ///
+ /// Результат генерации изображения
+ ///
+ public class GenerationResult
+ {
+ public bool Success { get; set; }
+ public byte[] ImageData { get; set; }
+ public string ErrorMessage { get; set; }
+ public string SavedPath { get; set; }
+ public GenerationRequest Request { get; set; }
+
+ public static GenerationResult Failure(string error)
+ {
+ return new GenerationResult { Success = false, ErrorMessage = error };
+ }
+
+ public static GenerationResult SuccessResult(
+ byte[] imageData,
+ string savedPath,
+ GenerationRequest request
+ )
+ {
+ return new GenerationResult
+ {
+ Success = true,
+ ImageData = imageData,
+ SavedPath = savedPath,
+ Request = request,
+ };
+ }
+ }
+}
diff --git a/Source/AIImages/Models/PawnAppearanceData.cs b/Source/AIImages/Models/PawnAppearanceData.cs
new file mode 100644
index 0000000..41c3e91
--- /dev/null
+++ b/Source/AIImages/Models/PawnAppearanceData.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace AIImages.Models
+{
+ ///
+ /// Модель данных внешности персонажа для генерации промптов
+ ///
+ public class PawnAppearanceData
+ {
+ public string Name { get; set; }
+ public Gender Gender { get; set; }
+ public int Age { get; set; }
+ public string BodyType { get; set; }
+ public Color SkinColor { get; set; }
+ public string HairStyle { get; set; }
+ public Color HairColor { get; set; }
+ public List Traits { get; set; }
+ public List Apparel { get; set; }
+
+ public PawnAppearanceData()
+ {
+ Traits = new List();
+ Apparel = new List();
+ }
+ }
+
+ ///
+ /// Данные об одежде персонажа
+ ///
+ public class ApparelData
+ {
+ public string Label { get; set; }
+ public string Material { get; set; }
+ public QualityCategory? Quality { get; set; }
+ public Color Color { get; set; }
+ public string LayerType { get; set; }
+ public int Durability { get; set; }
+ public int MaxDurability { get; set; }
+
+ public float DurabilityPercent =>
+ MaxDurability > 0 ? (float)Durability / MaxDurability : 1f;
+ }
+}
diff --git a/Source/AIImages/Models/StableDiffusionSettings.cs b/Source/AIImages/Models/StableDiffusionSettings.cs
new file mode 100644
index 0000000..0b684de
--- /dev/null
+++ b/Source/AIImages/Models/StableDiffusionSettings.cs
@@ -0,0 +1,62 @@
+namespace AIImages.Models
+{
+ ///
+ /// Настройки для генерации изображений через Stable Diffusion
+ ///
+ public class StableDiffusionSettings
+ {
+ public string PositivePrompt { get; set; }
+ public string NegativePrompt { get; set; }
+ public int Steps { get; set; }
+ public float CfgScale { get; set; }
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public string Sampler { get; set; }
+ public int Seed { get; set; }
+ public string Model { get; set; }
+ public ArtStyle ArtStyle { get; set; }
+ public ShotType ShotType { get; set; }
+
+ public StableDiffusionSettings()
+ {
+ // Значения по умолчанию
+ Steps = 30;
+ CfgScale = 7.5f;
+ Width = 512;
+ Height = 768;
+ Sampler = "Euler a";
+ Seed = -1; // Случайный seed
+ ArtStyle = ArtStyle.Realistic;
+ ShotType = ShotType.Portrait;
+ PositivePrompt = "";
+ NegativePrompt = "ugly, deformed, low quality, blurry, bad anatomy, worst quality";
+ }
+ }
+
+ ///
+ /// Художественный стиль изображения
+ ///
+ public enum ArtStyle
+ {
+ Realistic,
+ SemiRealistic,
+ Anime,
+ ConceptArt,
+ DigitalPainting,
+ OilPainting,
+ Sketch,
+ CellShaded,
+ }
+
+ ///
+ /// Тип кадра/композиции
+ ///
+ public enum ShotType
+ {
+ Portrait, // Портрет (голова и плечи)
+ HalfBody, // Половина тела
+ FullBody, // Полное тело
+ CloseUp, // Крупный план
+ ThreeQuarter, // Три четверти
+ }
+}
diff --git a/Source/AIImages/Services/AdvancedPromptGenerator.cs b/Source/AIImages/Services/AdvancedPromptGenerator.cs
new file mode 100644
index 0000000..cda4066
--- /dev/null
+++ b/Source/AIImages/Services/AdvancedPromptGenerator.cs
@@ -0,0 +1,353 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using AIImages.Models;
+using RimWorld;
+using Verse;
+
+namespace AIImages.Services
+{
+ ///
+ /// Продвинутый генератор промптов для Stable Diffusion
+ ///
+ public class AdvancedPromptGenerator : IPromptGeneratorService
+ {
+ private static readonly Dictionary TraitToMood = new Dictionary<
+ string,
+ string
+ >
+ {
+ { "Kind", "warm smile, gentle expression, friendly eyes" },
+ { "Bloodlust", "intense gaze, fierce expression, aggressive posture" },
+ { "Psychopath", "cold eyes, emotionless face, calculating expression" },
+ { "Pessimist", "sad eyes, worried expression, downcast gaze" },
+ { "Optimist", "bright smile, hopeful expression, cheerful demeanor" },
+ { "Nervous", "anxious expression, tense posture, worried eyes" },
+ { "Careful", "focused expression, attentive gaze, composed posture" },
+ { "Brave", "confident expression, determined gaze, strong posture" },
+ { "Wimp", "fearful eyes, nervous expression, timid posture" },
+ { "Greedy", "cunning expression, calculating eyes, sly smile" },
+ { "Jealous", "envious gaze, bitter expression, tense face" },
+ { "Ascetic", "serene expression, calm demeanor, peaceful eyes" },
+ { "Beautiful", "stunning features, attractive appearance, graceful" },
+ { "Ugly", "rough features, weathered appearance" },
+ { "Pretty", "attractive features, pleasant appearance, charming" },
+ };
+
+ private static readonly Dictionary ArtStylePrompts = new Dictionary<
+ ArtStyle,
+ string
+ >
+ {
+ { ArtStyle.Realistic, "photorealistic, hyperrealistic, realistic photo, photography" },
+ { ArtStyle.SemiRealistic, "semi-realistic, detailed illustration, realistic art" },
+ { ArtStyle.Anime, "anime style, manga style, anime character" },
+ {
+ ArtStyle.ConceptArt,
+ "concept art, digital art, artstation, professional concept design"
+ },
+ { ArtStyle.DigitalPainting, "digital painting, painterly, brush strokes, artistic" },
+ { ArtStyle.OilPainting, "oil painting, traditional painting, canvas, fine art" },
+ { ArtStyle.Sketch, "pencil sketch, hand drawn, sketch art, line art" },
+ { ArtStyle.CellShaded, "cell shaded, flat colors, toon shading, stylized" },
+ };
+
+ private static readonly Dictionary ShotTypePrompts = new Dictionary<
+ ShotType,
+ string
+ >
+ {
+ { ShotType.Portrait, "portrait, head and shoulders" },
+ { ShotType.HalfBody, "half body shot, waist up" },
+ { ShotType.FullBody, "full body, full length" },
+ { ShotType.CloseUp, "close up, face focus, detailed face" },
+ { ShotType.ThreeQuarter, "three-quarter view, 3/4 view" },
+ };
+
+ public string GeneratePositivePrompt(
+ PawnAppearanceData appearanceData,
+ StableDiffusionSettings settings
+ )
+ {
+ if (appearanceData == null)
+ return "portrait of a person";
+
+ StringBuilder prompt = new StringBuilder();
+
+ // 1. Художественный стиль
+ if (ArtStylePrompts.TryGetValue(settings.ArtStyle, out string stylePrompt))
+ {
+ prompt.Append(stylePrompt);
+ prompt.Append(", ");
+ }
+
+ // 2. Тип кадра
+ if (ShotTypePrompts.TryGetValue(settings.ShotType, out string shotPrompt))
+ {
+ prompt.Append(shotPrompt);
+ prompt.Append(" of ");
+ }
+
+ // 3. Базовое описание (возраст и пол)
+ prompt.Append(GetAgeAndGenderDescription(appearanceData));
+ prompt.Append(", ");
+
+ // 4. Тип тела
+ string bodyType = GetBodyTypeDescription(appearanceData.BodyType);
+ if (!string.IsNullOrEmpty(bodyType))
+ {
+ prompt.Append(bodyType);
+ prompt.Append(", ");
+ }
+
+ // 5. Цвет кожи
+ string skinTone = ColorDescriptionService.GetSkinToneDescription(
+ appearanceData.SkinColor
+ );
+ prompt.Append(skinTone);
+ prompt.Append(", ");
+
+ // 6. Волосы
+ string hairDescription = GetHairDescription(appearanceData);
+ if (!string.IsNullOrEmpty(hairDescription))
+ {
+ prompt.Append(hairDescription);
+ prompt.Append(", ");
+ }
+
+ // 7. Настроение и выражение на основе черт характера
+ string moodDescription = GetMoodFromTraits(appearanceData.Traits);
+ if (!string.IsNullOrEmpty(moodDescription))
+ {
+ prompt.Append(moodDescription);
+ prompt.Append(", ");
+ }
+
+ // 8. Одежда
+ string apparelDescription = GetApparelDescription(appearanceData.Apparel);
+ if (!string.IsNullOrEmpty(apparelDescription))
+ {
+ prompt.Append(apparelDescription);
+ prompt.Append(", ");
+ }
+
+ // 9. Базовый пользовательский промпт (если указан)
+ if (!string.IsNullOrEmpty(settings.PositivePrompt))
+ {
+ prompt.Append(settings.PositivePrompt);
+ prompt.Append(", ");
+ }
+
+ // 10. Качественные теги
+ prompt.Append(GetQualityTags(settings.ArtStyle));
+
+ return prompt.ToString().Trim().TrimEnd(',');
+ }
+
+ public string GenerateNegativePrompt(StableDiffusionSettings settings)
+ {
+ StringBuilder negativePrompt = new StringBuilder();
+
+ // Базовые негативные промпты
+ negativePrompt.Append(
+ "ugly, deformed, low quality, blurry, bad anatomy, worst quality, "
+ );
+ negativePrompt.Append(
+ "mutated, disfigured, bad proportions, extra limbs, missing limbs, "
+ );
+
+ // Специфичные для стиля негативы
+ if (
+ settings.ArtStyle == ArtStyle.Realistic
+ || settings.ArtStyle == ArtStyle.SemiRealistic
+ )
+ {
+ negativePrompt.Append("cartoon, anime, painting, drawing, illustration, ");
+ }
+ else if (settings.ArtStyle == ArtStyle.Anime)
+ {
+ negativePrompt.Append("realistic, photo, photography, 3d, ");
+ }
+
+ // Пользовательский негативный промпт
+ if (!string.IsNullOrEmpty(settings.NegativePrompt))
+ {
+ negativePrompt.Append(settings.NegativePrompt);
+ }
+
+ return negativePrompt.ToString().Trim().TrimEnd(',');
+ }
+
+ public string GetFullPromptDescription(
+ PawnAppearanceData appearanceData,
+ StableDiffusionSettings settings
+ )
+ {
+ StringBuilder description = new StringBuilder();
+
+ description.AppendLine("=== Positive Prompt ===");
+ description.AppendLine(GeneratePositivePrompt(appearanceData, settings));
+ description.AppendLine();
+
+ description.AppendLine("=== Negative Prompt ===");
+ description.AppendLine(GenerateNegativePrompt(settings));
+ description.AppendLine();
+
+ description.AppendLine("=== Technical Parameters ===");
+ description.AppendLine($"Steps: {settings.Steps}");
+ description.AppendLine($"CFG Scale: {settings.CfgScale}");
+ description.AppendLine($"Size: {settings.Width}x{settings.Height}");
+ description.AppendLine($"Sampler: {settings.Sampler}");
+ description.AppendLine(
+ $"Seed: {(settings.Seed == -1 ? "Random" : settings.Seed.ToString())}"
+ );
+
+ return description.ToString();
+ }
+
+ private string GetAgeAndGenderDescription(PawnAppearanceData data)
+ {
+ string ageGroup = data.Age switch
+ {
+ < 18 => "young",
+ < 25 => "young adult",
+ < 35 => "adult",
+ < 50 => "middle-aged",
+ < 65 => "mature",
+ _ => "elderly",
+ };
+
+ string genderLabel = data.Gender switch
+ {
+ Gender.Male => "man",
+ Gender.Female => "woman",
+ _ => "person",
+ };
+ return $"{ageGroup} {genderLabel}";
+ }
+
+ private string GetBodyTypeDescription(string bodyType)
+ {
+ if (string.IsNullOrEmpty(bodyType))
+ return "";
+
+ return bodyType.ToLower() switch
+ {
+ "thin" => "slender build, lean physique",
+ "hulk" => "muscular build, strong physique, athletic body",
+ "fat" => "heavyset build, stocky physique",
+ "female" => "feminine build",
+ "male" => "masculine build",
+ _ => "average build",
+ };
+ }
+
+ private string GetHairDescription(PawnAppearanceData data)
+ {
+ if (string.IsNullOrEmpty(data.HairStyle))
+ return "";
+
+ StringBuilder hair = new StringBuilder();
+
+ // Цвет волос
+ string hairColor = ColorDescriptionService.GetHairColorDescription(data.HairColor);
+ hair.Append(hairColor);
+ hair.Append(" ");
+
+ // Стиль прически (упрощаем сложные названия)
+ string style = data
+ .HairStyle.ToLower()
+ .Replace("_", " ")
+ .Replace("shaved", "very short")
+ .Replace("mohawk", "mohawk hairstyle");
+
+ hair.Append(style);
+ hair.Append(" hair");
+
+ return hair.ToString();
+ }
+
+ private string GetMoodFromTraits(List traits)
+ {
+ if (traits == null || !traits.Any())
+ return "neutral expression";
+
+ // Ищем черты, которые влияют на внешний вид
+ foreach (var trait in traits)
+ {
+ string traitDefName = trait.def.defName;
+ if (TraitToMood.TryGetValue(traitDefName, out string mood))
+ {
+ return mood;
+ }
+ }
+
+ return "calm expression";
+ }
+
+ private string GetApparelDescription(List apparel)
+ {
+ if (apparel == null || !apparel.Any())
+ return "simple clothes";
+
+ StringBuilder apparelDesc = new StringBuilder("wearing ");
+
+ // Берем топ 5 наиболее заметных предметов одежды
+ var visibleApparel = apparel.Take(5).ToList();
+
+ List items = new List();
+ foreach (var item in visibleApparel)
+ {
+ StringBuilder itemDesc = new StringBuilder();
+
+ // Цвет (если не белый)
+ if (item.Color != UnityEngine.Color.white)
+ {
+ string colorDesc = ColorDescriptionService.GetApparelColorDescription(
+ item.Color
+ );
+ itemDesc.Append(colorDesc);
+ itemDesc.Append(" ");
+ }
+
+ // Материал
+ if (!string.IsNullOrEmpty(item.Material))
+ {
+ itemDesc.Append(item.Material.ToLower());
+ itemDesc.Append(" ");
+ }
+
+ // Название предмета
+ itemDesc.Append(item.Label.ToLower());
+
+ items.Add(itemDesc.ToString());
+ }
+
+ apparelDesc.Append(string.Join(", ", items));
+
+ return apparelDesc.ToString();
+ }
+
+ private string GetQualityTags(ArtStyle style)
+ {
+ var baseTags = "highly detailed, professional, masterpiece, best quality";
+
+ if (style == ArtStyle.Realistic || style == ArtStyle.SemiRealistic)
+ {
+ return $"{baseTags}, professional photography, 8k uhd, dslr, high quality, sharp focus";
+ }
+ else if (style == ArtStyle.Anime)
+ {
+ return $"{baseTags}, anime masterpiece, high resolution, vibrant colors";
+ }
+ else if (style == ArtStyle.ConceptArt)
+ {
+ return $"{baseTags}, trending on artstation, professional digital art";
+ }
+ else
+ {
+ return baseTags;
+ }
+ }
+ }
+}
diff --git a/Source/AIImages/Services/ColorDescriptionService.cs b/Source/AIImages/Services/ColorDescriptionService.cs
new file mode 100644
index 0000000..5e1c5c7
--- /dev/null
+++ b/Source/AIImages/Services/ColorDescriptionService.cs
@@ -0,0 +1,165 @@
+using UnityEngine;
+
+namespace AIImages.Services
+{
+ ///
+ /// Сервис для умного определения цветов (вместо RGB значений)
+ ///
+ public static class ColorDescriptionService
+ {
+ ///
+ /// Получает текстовое описание цвета волос
+ ///
+ public static string GetHairColorDescription(Color color)
+ {
+ float h,
+ s,
+ v;
+ Color.RGBToHSV(color, out h, out s, out v);
+
+ // Проверяем на оттенки серого
+ if (s < 0.15f)
+ {
+ return GetGrayscaleDescription(v);
+ }
+
+ // Определяем оттенок
+ string hueDescription = GetHueDescription(h);
+ string brightnessModifier = GetBrightnessModifier(v, s);
+
+ return $"{brightnessModifier}{hueDescription}".Trim();
+ }
+
+ ///
+ /// Получает текстовое описание цвета кожи
+ ///
+ public static string GetSkinToneDescription(Color color)
+ {
+ // Вычисляем яркость
+ float brightness = (color.r + color.g + color.b) / 3f;
+
+ // Определяем оттенок кожи
+ if (brightness >= 0.85f)
+ return "very fair skin";
+ else if (brightness >= 0.75f)
+ return "fair skin";
+ else if (brightness >= 0.65f)
+ return "light skin";
+ else if (brightness >= 0.55f)
+ return "medium skin";
+ else if (brightness >= 0.45f)
+ return "olive skin";
+ else if (brightness >= 0.35f)
+ return "tan skin";
+ else if (brightness >= 0.25f)
+ return "brown skin";
+ else if (brightness >= 0.15f)
+ return "dark brown skin";
+ else
+ return "very dark skin";
+ }
+
+ ///
+ /// Получает описание цвета одежды
+ ///
+ public static string GetApparelColorDescription(Color color)
+ {
+ float h,
+ s,
+ v;
+ Color.RGBToHSV(color, out h, out s, out v);
+
+ // Проверяем на оттенки серого
+ if (s < 0.1f)
+ {
+ return GetGrayscaleDescription(v);
+ }
+
+ // Определяем оттенок
+ string hueDescription = GetHueDescription(h);
+
+ // Модификаторы насыщенности и яркости
+ string saturationModifier = "";
+ if (s < 0.3f)
+ saturationModifier = "pale ";
+ else if (s > 0.8f)
+ saturationModifier = "vivid ";
+
+ string brightnessModifier = "";
+ if (v < 0.3f)
+ brightnessModifier = "dark ";
+ else if (v > 0.8f)
+ brightnessModifier = "bright ";
+
+ return $"{brightnessModifier}{saturationModifier}{hueDescription}".Trim();
+ }
+
+ private static string GetGrayscaleDescription(float value)
+ {
+ if (value >= 0.95f)
+ return "white";
+ else if (value >= 0.8f)
+ return "light gray";
+ else if (value >= 0.6f)
+ return "gray";
+ else if (value >= 0.4f)
+ return "dark gray";
+ else if (value >= 0.2f)
+ return "charcoal";
+ else
+ return "black";
+ }
+
+ private static string GetHueDescription(float hue)
+ {
+ // Hue from 0 to 1
+ if (hue < 0.05f || hue >= 0.95f)
+ return "red";
+ if (hue < 0.083f)
+ return "orange-red";
+ if (hue < 0.15f)
+ return "orange";
+ if (hue < 0.19f)
+ return "golden";
+ if (hue < 0.22f)
+ return "yellow";
+ if (hue < 0.35f)
+ return "yellow-green";
+ if (hue < 0.45f)
+ return "green";
+ if (hue < 0.52f)
+ return "cyan";
+ if (hue < 0.58f)
+ return "light blue";
+ if (hue < 0.65f)
+ return "blue";
+ if (hue < 0.72f)
+ return "deep blue";
+ if (hue < 0.78f)
+ return "purple";
+ if (hue < 0.85f)
+ return "violet";
+ if (hue < 0.92f)
+ return "magenta";
+
+ return "pink";
+ }
+
+ private static string GetBrightnessModifier(float value, float saturation)
+ {
+ // Для волос используем специальные термины
+ if (value < 0.2f)
+ return "jet black ";
+ else if (value < 0.3f)
+ return "black ";
+ else if (value < 0.45f)
+ return "dark ";
+ else if (value > 0.85f && saturation < 0.3f)
+ return "platinum ";
+ else if (value > 0.75f)
+ return "light ";
+ else
+ return "";
+ }
+ }
+}
diff --git a/Source/AIImages/Services/IPawnDescriptionService.cs b/Source/AIImages/Services/IPawnDescriptionService.cs
new file mode 100644
index 0000000..d8948f4
--- /dev/null
+++ b/Source/AIImages/Services/IPawnDescriptionService.cs
@@ -0,0 +1,26 @@
+using AIImages.Models;
+using Verse;
+
+namespace AIImages.Services
+{
+ ///
+ /// Интерфейс сервиса для извлечения данных о внешности персонажа
+ ///
+ public interface IPawnDescriptionService
+ {
+ ///
+ /// Извлекает данные о внешности персонажа
+ ///
+ PawnAppearanceData ExtractAppearanceData(Pawn pawn);
+
+ ///
+ /// Получает текстовое описание внешности для отображения в UI
+ ///
+ string GetAppearanceDescription(Pawn pawn);
+
+ ///
+ /// Получает текстовое описание одежды для отображения в UI
+ ///
+ string GetApparelDescription(Pawn pawn);
+ }
+}
diff --git a/Source/AIImages/Services/IPromptGeneratorService.cs b/Source/AIImages/Services/IPromptGeneratorService.cs
new file mode 100644
index 0000000..34e4301
--- /dev/null
+++ b/Source/AIImages/Services/IPromptGeneratorService.cs
@@ -0,0 +1,31 @@
+using AIImages.Models;
+
+namespace AIImages.Services
+{
+ ///
+ /// Интерфейс сервиса для генерации промптов Stable Diffusion
+ ///
+ public interface IPromptGeneratorService
+ {
+ ///
+ /// Генерирует позитивный промпт на основе данных о персонаже
+ ///
+ string GeneratePositivePrompt(
+ PawnAppearanceData appearanceData,
+ StableDiffusionSettings settings
+ );
+
+ ///
+ /// Генерирует негативный промпт на основе настроек
+ ///
+ string GenerateNegativePrompt(StableDiffusionSettings settings);
+
+ ///
+ /// Получает полное описание промпта (позитивный + негативный) для отображения
+ ///
+ string GetFullPromptDescription(
+ PawnAppearanceData appearanceData,
+ StableDiffusionSettings settings
+ );
+ }
+}
diff --git a/Source/AIImages/Services/IStableDiffusionApiService.cs b/Source/AIImages/Services/IStableDiffusionApiService.cs
new file mode 100644
index 0000000..247175c
--- /dev/null
+++ b/Source/AIImages/Services/IStableDiffusionApiService.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AIImages.Models;
+
+namespace AIImages.Services
+{
+ ///
+ /// Интерфейс сервиса для работы с Stable Diffusion API
+ ///
+ public interface IStableDiffusionApiService
+ {
+ ///
+ /// Генерирует изображение на основе запроса
+ ///
+ Task GenerateImageAsync(GenerationRequest request);
+
+ ///
+ /// Проверяет доступность API
+ ///
+ Task CheckApiAvailability(string apiEndpoint);
+
+ ///
+ /// Получает список доступных моделей с API
+ ///
+ Task> GetAvailableModels(string apiEndpoint);
+
+ ///
+ /// Получает список доступных сэмплеров
+ ///
+ Task> GetAvailableSamplers(string apiEndpoint);
+ }
+}
diff --git a/Source/AIImages/Services/PawnDescriptionService.cs b/Source/AIImages/Services/PawnDescriptionService.cs
new file mode 100644
index 0000000..2dace17
--- /dev/null
+++ b/Source/AIImages/Services/PawnDescriptionService.cs
@@ -0,0 +1,183 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using AIImages.Models;
+using RimWorld;
+using Verse;
+
+namespace AIImages.Services
+{
+ ///
+ /// Сервис для извлечения и описания данных о внешности персонажа
+ ///
+ public class PawnDescriptionService : IPawnDescriptionService
+ {
+ public PawnAppearanceData ExtractAppearanceData(Pawn pawn)
+ {
+ if (pawn?.story == null)
+ return null;
+
+ var data = new PawnAppearanceData
+ {
+ Name = pawn.Name?.ToStringShort ?? "Unknown",
+ Gender = pawn.gender,
+ Age = pawn.ageTracker.AgeBiologicalYears,
+ BodyType = pawn.story.bodyType?.defName,
+ SkinColor = pawn.story.SkinColor,
+ HairStyle = pawn.story.hairDef?.label,
+ HairColor = pawn.story.HairColor,
+ };
+
+ // Извлекаем черты характера
+ if (pawn.story.traits?.allTraits != null)
+ {
+ data.Traits.AddRange(pawn.story.traits.allTraits);
+ }
+
+ // Извлекаем одежду
+ if (pawn.apparel?.WornApparel != null)
+ {
+ foreach (var apparel in pawn.apparel.WornApparel)
+ {
+ var apparelData = new ApparelData
+ {
+ Label = apparel.def.label,
+ Material = apparel.Stuff?.label,
+ Color = apparel.DrawColor,
+ LayerType = apparel.def.apparel?.LastLayer.ToString(),
+ Durability = apparel.HitPoints,
+ MaxDurability = apparel.MaxHitPoints,
+ };
+
+ if (apparel.TryGetQuality(out QualityCategory quality))
+ {
+ apparelData.Quality = quality;
+ }
+
+ data.Apparel.Add(apparelData);
+ }
+ }
+
+ return data;
+ }
+
+ public string GetAppearanceDescription(Pawn pawn)
+ {
+ 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)
+ {
+ string skinDescription = ColorDescriptionService.GetSkinToneDescription(
+ pawn.story.SkinColor
+ );
+ sb.AppendLine("AIImages.Appearance.SkinTone".Translate(skinDescription));
+ }
+
+ // Волосы
+ if (pawn.story.hairDef != null)
+ {
+ sb.AppendLine("AIImages.Appearance.Hairstyle".Translate(pawn.story.hairDef.label));
+ if (pawn.story.HairColor != null)
+ {
+ string hairColorDescription = ColorDescriptionService.GetHairColorDescription(
+ pawn.story.HairColor
+ );
+ sb.AppendLine(
+ "AIImages.Appearance.HairColorDesc".Translate(hairColorDescription)
+ );
+ }
+ }
+
+ // Черты характера
+ 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();
+ }
+
+ public string GetApparelDescription(Pawn pawn)
+ {
+ 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 != UnityEngine.Color.white)
+ {
+ string colorDesc = ColorDescriptionService.GetApparelColorDescription(
+ apparel.DrawColor
+ );
+ sb.AppendLine("AIImages.Apparel.ColorDesc".Translate(colorDesc));
+ }
+
+ sb.AppendLine();
+ }
+ }
+}
diff --git a/Source/AIImages/Services/StableDiffusionApiService.cs b/Source/AIImages/Services/StableDiffusionApiService.cs
new file mode 100644
index 0000000..34a409a
--- /dev/null
+++ b/Source/AIImages/Services/StableDiffusionApiService.cs
@@ -0,0 +1,234 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using AIImages.Models;
+using Newtonsoft.Json;
+using Verse;
+
+namespace AIImages.Services
+{
+ ///
+ /// Сервис для работы с Stable Diffusion API (AUTOMATIC1111 WebUI)
+ ///
+ public class StableDiffusionApiService : IStableDiffusionApiService
+ {
+ private readonly HttpClient httpClient;
+ private readonly string saveFolderPath;
+
+ public StableDiffusionApiService(string savePath = "AIImages/Generated")
+ {
+ httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
+
+ // Определяем путь для сохранения
+ saveFolderPath = Path.Combine(GenFilePaths.SaveDataFolderPath, savePath);
+
+ // Создаем папку, если не существует
+ if (!Directory.Exists(saveFolderPath))
+ {
+ Directory.CreateDirectory(saveFolderPath);
+ }
+ }
+
+ public async Task GenerateImageAsync(GenerationRequest request)
+ {
+ try
+ {
+ Log.Message(
+ $"[AI Images] Starting image generation with prompt: {request.Prompt.Substring(0, Math.Min(50, request.Prompt.Length))}..."
+ );
+
+ // Формируем JSON запрос для AUTOMATIC1111 API
+ var apiRequest = new
+ {
+ prompt = request.Prompt,
+ negative_prompt = request.NegativePrompt,
+ steps = request.Steps,
+ cfg_scale = request.CfgScale,
+ width = request.Width,
+ height = request.Height,
+ sampler_name = request.Sampler,
+ seed = request.Seed,
+ save_images = false,
+ send_images = true,
+ };
+
+ string jsonRequest = JsonConvert.SerializeObject(apiRequest);
+ var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
+
+ // Отправляем запрос
+ string endpoint = $"{request.Model}/sdapi/v1/txt2img";
+ HttpResponseMessage response = await httpClient.PostAsync(endpoint, content);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ string errorContent = await response.Content.ReadAsStringAsync();
+ Log.Error(
+ $"[AI Images] API request failed: {response.StatusCode} - {errorContent}"
+ );
+ return GenerationResult.Failure($"API Error: {response.StatusCode}");
+ }
+
+ string jsonResponse = await response.Content.ReadAsStringAsync();
+ var apiResponse = JsonConvert.DeserializeObject(jsonResponse);
+
+ if (apiResponse?.images == null || apiResponse.images.Length == 0)
+ {
+ return GenerationResult.Failure("No images returned from API");
+ }
+
+ // Декодируем изображение из base64
+ byte[] imageData = Convert.FromBase64String(apiResponse.images[0]);
+
+ // Сохраняем изображение
+ string fileName = $"pawn_{DateTime.Now:yyyyMMdd_HHmmss}.png";
+ string fullPath = Path.Combine(saveFolderPath, fileName);
+ await File.WriteAllBytesAsync(fullPath, imageData);
+
+ Log.Message($"[AI Images] Image generated successfully and saved to: {fullPath}");
+
+ return GenerationResult.SuccessResult(imageData, fullPath, request);
+ }
+ catch (TaskCanceledException)
+ {
+ return GenerationResult.Failure("Request timeout. Generation took too long.");
+ }
+ catch (HttpRequestException ex)
+ {
+ Log.Error($"[AI Images] HTTP error: {ex.Message}");
+ return GenerationResult.Failure($"Connection error: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[AI Images] Unexpected error: {ex.Message}\n{ex.StackTrace}");
+ return GenerationResult.Failure($"Error: {ex.Message}");
+ }
+ }
+
+ public async Task CheckApiAvailability(string apiEndpoint)
+ {
+ try
+ {
+ string endpoint = $"{apiEndpoint}/sdapi/v1/sd-models";
+ HttpResponseMessage response = await httpClient.GetAsync(endpoint);
+ return response.IsSuccessStatusCode;
+ }
+ catch (Exception ex)
+ {
+ Log.Warning($"[AI Images] API check failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ public async Task> GetAvailableModels(string apiEndpoint)
+ {
+ try
+ {
+ string endpoint = $"{apiEndpoint}/sdapi/v1/sd-models";
+ HttpResponseMessage response = await httpClient.GetAsync(endpoint);
+
+ if (!response.IsSuccessStatusCode)
+ return new List();
+
+ string jsonResponse = await response.Content.ReadAsStringAsync();
+ var models = JsonConvert.DeserializeObject>(jsonResponse);
+
+ var modelNames = new List();
+ if (models != null)
+ {
+ foreach (var model in models)
+ {
+ modelNames.Add(model.title ?? model.model_name);
+ }
+ }
+
+ Log.Message($"[AI Images] Found {modelNames.Count} models");
+ return modelNames;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[AI Images] Failed to load models: {ex.Message}");
+ return new List();
+ }
+ }
+
+ public async Task> GetAvailableSamplers(string apiEndpoint)
+ {
+ try
+ {
+ string endpoint = $"{apiEndpoint}/sdapi/v1/samplers";
+ HttpResponseMessage response = await httpClient.GetAsync(endpoint);
+
+ if (!response.IsSuccessStatusCode)
+ return GetDefaultSamplers();
+
+ string jsonResponse = await response.Content.ReadAsStringAsync();
+ var samplers = JsonConvert.DeserializeObject>(jsonResponse);
+
+ var samplerNames = new List();
+ if (samplers != null)
+ {
+ foreach (var sampler in samplers)
+ {
+ samplerNames.Add(sampler.name);
+ }
+ }
+
+ Log.Message($"[AI Images] Found {samplerNames.Count} samplers");
+ return samplerNames;
+ }
+ catch (Exception ex)
+ {
+ Log.Warning($"[AI Images] Failed to load samplers: {ex.Message}");
+ return GetDefaultSamplers();
+ }
+ }
+
+ private List GetDefaultSamplers()
+ {
+ return new List
+ {
+ "Euler a",
+ "Euler",
+ "LMS",
+ "Heun",
+ "DPM2",
+ "DPM2 a",
+ "DPM++ 2S a",
+ "DPM++ 2M",
+ "DPM++ SDE",
+ "DPM fast",
+ "DPM adaptive",
+ "LMS Karras",
+ "DPM2 Karras",
+ "DPM2 a Karras",
+ "DPM++ 2S a Karras",
+ "DPM++ 2M Karras",
+ "DPM++ SDE Karras",
+ "DDIM",
+ "PLMS",
+ };
+ }
+
+ // Вспомогательные классы для десериализации JSON ответов
+#pragma warning disable S3459, S1144 // Properties set by JSON deserializer
+ private sealed class Txt2ImgResponse
+ {
+ public string[] images { get; set; }
+ }
+
+ private sealed class SdModel
+ {
+ public string title { get; set; }
+ public string model_name { get; set; }
+ }
+
+ private sealed class SdSampler
+ {
+ public string name { get; set; }
+ }
+#pragma warning restore S3459, S1144
+ }
+}
diff --git a/Source/AIImages/Settings/AIImagesModSettings.cs b/Source/AIImages/Settings/AIImagesModSettings.cs
new file mode 100644
index 0000000..621726e
--- /dev/null
+++ b/Source/AIImages/Settings/AIImagesModSettings.cs
@@ -0,0 +1,94 @@
+using AIImages.Models;
+using Verse;
+
+namespace AIImages.Settings
+{
+ ///
+ /// Настройки мода AI Images
+ ///
+#pragma warning disable S1104 // Fields required for RimWorld's Scribe serialization system
+ public class AIImagesModSettings : ModSettings
+ {
+ // API настройки
+ public string apiEndpoint = "http://127.0.0.1:7860";
+ public string selectedModel = "";
+ public string selectedSampler = "Euler a";
+
+ // Настройки генерации
+ public int steps = 30;
+ public float cfgScale = 7.5f;
+ public int width = 512;
+ public int height = 768;
+ public int seed = -1;
+
+ // Промпты
+ public string basePositivePrompt = "";
+ public string baseNegativePrompt =
+ "ugly, deformed, low quality, blurry, bad anatomy, worst quality";
+
+ // Художественный стиль
+ public ArtStyle artStyle = ArtStyle.Realistic;
+ public ShotType shotType = ShotType.Portrait;
+
+ // Путь для сохранения
+ public string savePath = "AIImages/Generated";
+
+ // Флаги
+ public bool autoLoadModels = true;
+ public bool showTechnicalInfo = true;
+ public bool saveGenerationHistory = true;
+
+ public override void ExposeData()
+ {
+ Scribe_Values.Look(ref apiEndpoint, "apiEndpoint", "http://127.0.0.1:7860");
+ Scribe_Values.Look(ref selectedModel, "selectedModel", "");
+ Scribe_Values.Look(ref selectedSampler, "selectedSampler", "Euler a");
+
+ Scribe_Values.Look(ref steps, "steps", 30);
+ Scribe_Values.Look(ref cfgScale, "cfgScale", 7.5f);
+ Scribe_Values.Look(ref width, "width", 512);
+ Scribe_Values.Look(ref height, "height", 768);
+ Scribe_Values.Look(ref seed, "seed", -1);
+
+ Scribe_Values.Look(ref basePositivePrompt, "basePositivePrompt", "");
+ Scribe_Values.Look(
+ ref baseNegativePrompt,
+ "baseNegativePrompt",
+ "ugly, deformed, low quality, blurry, bad anatomy, worst quality"
+ );
+
+ Scribe_Values.Look(ref artStyle, "artStyle", ArtStyle.Realistic);
+ Scribe_Values.Look(ref shotType, "shotType", ShotType.Portrait);
+
+ Scribe_Values.Look(ref savePath, "savePath", "AIImages/Generated");
+
+ Scribe_Values.Look(ref autoLoadModels, "autoLoadModels", true);
+ Scribe_Values.Look(ref showTechnicalInfo, "showTechnicalInfo", true);
+ Scribe_Values.Look(ref saveGenerationHistory, "saveGenerationHistory", true);
+
+ base.ExposeData();
+ }
+
+ ///
+ /// Создает объект StableDiffusionSettings из настроек мода
+ ///
+ public StableDiffusionSettings ToStableDiffusionSettings()
+ {
+ return new StableDiffusionSettings
+ {
+ Steps = steps,
+ CfgScale = cfgScale,
+ Width = width,
+ Height = height,
+ Sampler = selectedSampler,
+ Seed = seed,
+ Model = selectedModel,
+ ArtStyle = artStyle,
+ ShotType = shotType,
+ PositivePrompt = basePositivePrompt,
+ NegativePrompt = baseNegativePrompt,
+ };
+ }
+ }
+#pragma warning restore S1104
+}
diff --git a/Source/AIImages/UI/AIImagesSettingsUI.cs b/Source/AIImages/UI/AIImagesSettingsUI.cs
new file mode 100644
index 0000000..f09e81a
--- /dev/null
+++ b/Source/AIImages/UI/AIImagesSettingsUI.cs
@@ -0,0 +1,284 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AIImages.Models;
+using AIImages.Settings;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace AIImages
+{
+ ///
+ /// UI для настроек мода в меню настроек RimWorld
+ ///
+ public static class AIImagesSettingsUI
+ {
+ private static Vector2 scrollPosition = Vector2.zero;
+ private static string stepsBuffer;
+ private static string widthBuffer;
+ private static string heightBuffer;
+
+ public static void DoSettingsWindowContents(Rect inRect, AIImagesModSettings settings)
+ {
+ // Инициализируем буферы при первом вызове
+ if (string.IsNullOrEmpty(stepsBuffer))
+ {
+ stepsBuffer = settings.steps.ToString();
+ widthBuffer = settings.width.ToString();
+ heightBuffer = settings.height.ToString();
+ }
+
+ Listing_Standard listingStandard = new Listing_Standard();
+ Rect viewRect = new Rect(0f, 0f, inRect.width - 20f, 1200f);
+
+ Widgets.BeginScrollView(inRect, ref scrollPosition, viewRect);
+ listingStandard.Begin(viewRect);
+
+ // === API Settings ===
+ listingStandard.Label(
+ "AIImages.Settings.ApiSection".Translate(),
+ -1f,
+ "AIImages.Settings.ApiSectionTooltip".Translate()
+ );
+ listingStandard.GapLine();
+
+ listingStandard.Label("AIImages.Settings.ApiEndpoint".Translate() + ":");
+ settings.apiEndpoint = listingStandard.TextEntry(settings.apiEndpoint);
+ listingStandard.Gap(8f);
+
+ // Кнопка проверки подключения
+ if (listingStandard.ButtonText("AIImages.Settings.TestConnection".Translate()))
+ {
+ _ = TestApiConnection(settings.apiEndpoint);
+ }
+
+ // Кнопка загрузки моделей
+ if (listingStandard.ButtonText("AIImages.Settings.LoadModels".Translate()))
+ {
+ _ = LoadModelsFromApi(settings);
+ }
+
+ listingStandard.Gap(12f);
+
+ // === Generation Settings ===
+ listingStandard.Label(
+ "AIImages.Settings.GenerationSection".Translate(),
+ -1f,
+ "AIImages.Settings.GenerationSectionTooltip".Translate()
+ );
+ listingStandard.GapLine();
+
+ // Art Style
+ if (
+ listingStandard.ButtonTextLabeled(
+ "AIImages.Settings.ArtStyle".Translate(),
+ settings.artStyle.ToString()
+ )
+ )
+ {
+ List styleOptions = new List();
+ foreach (ArtStyle style in Enum.GetValues(typeof(ArtStyle)))
+ {
+ ArtStyle localStyle = style;
+ styleOptions.Add(
+ new FloatMenuOption(style.ToString(), () => settings.artStyle = localStyle)
+ );
+ }
+ Find.WindowStack.Add(new FloatMenu(styleOptions));
+ }
+
+ // Shot Type
+ if (
+ listingStandard.ButtonTextLabeled(
+ "AIImages.Settings.ShotType".Translate(),
+ settings.shotType.ToString()
+ )
+ )
+ {
+ List shotOptions = new List();
+ foreach (ShotType shot in Enum.GetValues(typeof(ShotType)))
+ {
+ ShotType localShot = shot;
+ shotOptions.Add(
+ new FloatMenuOption(shot.ToString(), () => settings.shotType = localShot)
+ );
+ }
+ Find.WindowStack.Add(new FloatMenu(shotOptions));
+ }
+
+ listingStandard.Gap(8f);
+
+ // Steps
+ listingStandard.Label("AIImages.Settings.Steps".Translate() + $": {settings.steps}");
+ settings.steps = (int)listingStandard.Slider(settings.steps, 1, 150);
+ listingStandard.Gap(8f);
+
+ // CFG Scale
+ listingStandard.Label(
+ "AIImages.Settings.CfgScale".Translate() + $": {settings.cfgScale:F1}"
+ );
+ settings.cfgScale = listingStandard.Slider(settings.cfgScale, 1f, 30f);
+ listingStandard.Gap(8f);
+
+ // Width
+ listingStandard.Label("AIImages.Settings.Width".Translate() + ":");
+ widthBuffer = listingStandard.TextEntry(widthBuffer);
+ if (int.TryParse(widthBuffer, out int width))
+ {
+ settings.width = Mathf.Clamp(width, 64, 2048);
+ }
+
+ // Height
+ listingStandard.Label("AIImages.Settings.Height".Translate() + ":");
+ heightBuffer = listingStandard.TextEntry(heightBuffer);
+ if (int.TryParse(heightBuffer, out int height))
+ {
+ settings.height = Mathf.Clamp(height, 64, 2048);
+ }
+
+ // Common size presets
+ listingStandard.Gap(4f);
+ Rect presetRect = listingStandard.GetRect(30f);
+ if (Widgets.ButtonText(new Rect(presetRect.x, presetRect.y, 80f, 30f), "512x512"))
+ {
+ settings.width = 512;
+ settings.height = 512;
+ widthBuffer = "512";
+ heightBuffer = "512";
+ }
+ if (Widgets.ButtonText(new Rect(presetRect.x + 85f, presetRect.y, 80f, 30f), "512x768"))
+ {
+ settings.width = 512;
+ settings.height = 768;
+ widthBuffer = "512";
+ heightBuffer = "768";
+ }
+ if (
+ Widgets.ButtonText(new Rect(presetRect.x + 170f, presetRect.y, 80f, 30f), "768x768")
+ )
+ {
+ settings.width = 768;
+ settings.height = 768;
+ widthBuffer = "768";
+ heightBuffer = "768";
+ }
+
+ listingStandard.Gap(12f);
+
+ // Sampler
+ listingStandard.Label("AIImages.Settings.Sampler".Translate() + ":");
+ settings.selectedSampler = listingStandard.TextEntry(settings.selectedSampler);
+ listingStandard.Gap(12f);
+
+ // === Prompts ===
+ listingStandard.Label(
+ "AIImages.Settings.PromptsSection".Translate(),
+ -1f,
+ "AIImages.Settings.PromptsSectionTooltip".Translate()
+ );
+ listingStandard.GapLine();
+
+ listingStandard.Label("AIImages.Settings.BasePositivePrompt".Translate() + ":");
+ settings.basePositivePrompt = listingStandard.TextEntry(settings.basePositivePrompt, 3);
+ listingStandard.Gap(8f);
+
+ listingStandard.Label("AIImages.Settings.BaseNegativePrompt".Translate() + ":");
+ settings.baseNegativePrompt = listingStandard.TextEntry(settings.baseNegativePrompt, 3);
+ listingStandard.Gap(12f);
+
+ // === Options ===
+ listingStandard.Label("AIImages.Settings.OptionsSection".Translate());
+ listingStandard.GapLine();
+
+ listingStandard.CheckboxLabeled(
+ "AIImages.Settings.AutoLoadModels".Translate(),
+ ref settings.autoLoadModels
+ );
+ listingStandard.CheckboxLabeled(
+ "AIImages.Settings.ShowTechnicalInfo".Translate(),
+ ref settings.showTechnicalInfo
+ );
+ listingStandard.CheckboxLabeled(
+ "AIImages.Settings.SaveHistory".Translate(),
+ ref settings.saveGenerationHistory
+ );
+
+ listingStandard.Gap(12f);
+
+ // Save path
+ listingStandard.Label("AIImages.Settings.SavePath".Translate() + ":");
+ settings.savePath = listingStandard.TextEntry(settings.savePath);
+
+ listingStandard.End();
+ Widgets.EndScrollView();
+ }
+
+ private static async System.Threading.Tasks.Task TestApiConnection(string endpoint)
+ {
+ try
+ {
+ Log.Message($"[AI Images] Testing connection to {endpoint}...");
+ bool available = await AIImagesMod.ApiService.CheckApiAvailability(endpoint);
+
+ if (available)
+ {
+ Messages.Message(
+ "AIImages.Settings.ConnectionSuccess".Translate(),
+ MessageTypeDefOf.PositiveEvent
+ );
+ }
+ else
+ {
+ Messages.Message(
+ "AIImages.Settings.ConnectionFailed".Translate(),
+ MessageTypeDefOf.RejectInput
+ );
+ }
+ }
+ catch (Exception ex)
+ {
+ Messages.Message($"Error: {ex.Message}", MessageTypeDefOf.RejectInput);
+ }
+ }
+
+ private static async System.Threading.Tasks.Task LoadModelsFromApi(
+ AIImagesModSettings settings
+ )
+ {
+ try
+ {
+ Log.Message("[AI Images] Loading models from API...");
+ var models = await AIImagesMod.ApiService.GetAvailableModels(settings.apiEndpoint);
+
+ if (models.Count > 0)
+ {
+ Messages.Message(
+ "AIImages.Settings.ModelsLoaded".Translate(models.Count),
+ MessageTypeDefOf.PositiveEvent
+ );
+
+ // Если модель не выбрана, выбираем первую
+ if (string.IsNullOrEmpty(settings.selectedModel) && models.Count > 0)
+ {
+ settings.selectedModel = models[0];
+ }
+ }
+ else
+ {
+ Messages.Message(
+ "AIImages.Settings.NoModelsFound".Translate(),
+ MessageTypeDefOf.RejectInput
+ );
+ }
+ }
+ catch (Exception ex)
+ {
+ Messages.Message(
+ $"Error loading models: {ex.Message}",
+ MessageTypeDefOf.RejectInput
+ );
+ }
+ }
+ }
+}
diff --git a/Source/AIImages/Window_AIImage.cs b/Source/AIImages/Window_AIImage.cs
index 20a9576..901177e 100644
--- a/Source/AIImages/Window_AIImage.cs
+++ b/Source/AIImages/Window_AIImage.cs
@@ -1,6 +1,6 @@
-using System.Collections.Generic;
+using System;
using System.Linq;
-using System.Text;
+using AIImages.Models;
using RimWorld;
using UnityEngine;
using Verse;
@@ -10,7 +10,7 @@ using Verse;
namespace AIImages
{
///
- /// Empty window that opens when clicking the pawn button
+ /// Окно для просмотра персонажа и генерации AI изображений
///
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Style",
@@ -25,29 +25,48 @@ namespace AIImages
public class Window_AIImage : Window
{
private Pawn pawn;
+ private PawnAppearanceData appearanceData;
+ private StableDiffusionSettings generationSettings;
+
+ private Texture2D generatedImage;
+ private bool isGenerating = false;
+ private string generationStatus = "";
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; // Не блокируем управление камерой
+ this.forcePause = false;
+ this.absorbInputAroundWindow = false;
+ this.draggable = true;
+ this.preventCameraMotion = false;
+
+ // Извлекаем данные персонажа
+ RefreshPawnData();
}
- public override Vector2 InitialSize => new Vector2(700f, 700f);
+ public override Vector2 InitialSize => new Vector2(900f, 800f);
private Vector2 scrollPosition = Vector2.zero;
private float copiedMessageTime = 0f;
+ ///
+ /// Обновляет данные персонажа
+ ///
+ private void RefreshPawnData()
+ {
+ appearanceData = AIImagesMod.PawnDescriptionService.ExtractAppearanceData(pawn);
+ generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings();
+ }
+
///
/// Обновляет текущую пешку в окне
///
public void UpdatePawn(Pawn newPawn)
{
this.pawn = newPawn;
+ RefreshPawnData();
}
///
@@ -70,7 +89,7 @@ namespace AIImages
// Если выбрана новая колонистская пешка, обновляем окно
if (selectedPawn != null && selectedPawn != pawn)
{
- pawn = selectedPawn;
+ UpdatePawn(selectedPawn);
}
// Уменьшаем таймер сообщения о копировании
@@ -81,274 +100,71 @@ namespace AIImages
}
///
- /// Получает описание внешности персонажа
+ /// Асинхронная генерация изображения
///
- private string GetAppearanceDescription()
+ private async System.Threading.Tasks.Task GenerateImage()
{
- if (pawn?.story == null)
- return "AIImages.Appearance.NoInfo".Translate();
+ if (isGenerating)
+ return;
- StringBuilder sb = new StringBuilder();
+ isGenerating = true;
+ generationStatus = "AIImages.Generation.InProgress".Translate();
- // Пол
- sb.AppendLine("AIImages.Appearance.Gender".Translate(pawn.gender.GetLabel()));
-
- // Возраст
- sb.AppendLine("AIImages.Appearance.Age".Translate(pawn.ageTracker.AgeBiologicalYears));
-
- // Тип тела
- if (pawn.story.bodyType != null)
+ try
{
- sb.AppendLine(
- "AIImages.Appearance.BodyType".Translate(pawn.story.bodyType.defName)
+ // Генерируем промпты
+ string positivePrompt = AIImagesMod.PromptGeneratorService.GeneratePositivePrompt(
+ appearanceData,
+ generationSettings
);
- }
-
- // Цвет кожи
- 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")
- )
+ string negativePrompt = AIImagesMod.PromptGeneratorService.GenerateNegativePrompt(
+ generationSettings
);
- }
- // Волосы
- if (pawn.story.hairDef != null)
- {
- sb.AppendLine("AIImages.Appearance.Hairstyle".Translate(pawn.story.hairDef.label));
- if (pawn.story.HairColor != null)
+ // Создаем запрос
+ var request = new GenerationRequest
{
- sb.AppendLine(
- "AIImages.Appearance.HairColor".Translate(
- pawn.story.HairColor.r.ToString("F2"),
- pawn.story.HairColor.g.ToString("F2"),
- pawn.story.HairColor.b.ToString("F2")
- )
+ Prompt = positivePrompt,
+ NegativePrompt = negativePrompt,
+ Steps = generationSettings.Steps,
+ CfgScale = generationSettings.CfgScale,
+ Width = generationSettings.Width,
+ Height = generationSettings.Height,
+ Sampler = generationSettings.Sampler,
+ Seed = generationSettings.Seed,
+ Model = AIImagesMod.Settings.apiEndpoint,
+ };
+
+ // Генерируем изображение
+ var result = await AIImagesMod.ApiService.GenerateImageAsync(request);
+
+ if (result.Success)
+ {
+ // Загружаем текстуру
+ generatedImage = new Texture2D(2, 2);
+ generatedImage.LoadImage(result.ImageData);
+ generationStatus = "AIImages.Generation.Success".Translate();
+
+ Messages.Message(
+ "AIImages.Generation.SavedTo".Translate(result.SavedPath),
+ MessageTypeDefOf.PositiveEvent
);
}
- }
-
- // Черты характера
- 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)
+ else
{
- sb.AppendLine($" • {trait.LabelCap}");
+ generationStatus =
+ $"AIImages.Generation.Failed".Translate() + ": {result.ErrorMessage}";
+ Messages.Message(generationStatus, MessageTypeDefOf.RejectInput);
}
}
-
- 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())
+ catch (Exception ex)
{
- sb.AppendLine("AIImages.Apparel.NoClothes".Translate());
+ generationStatus = $"Error: {ex.Message}";
+ Log.Error($"[AI Images] Generation error: {ex}");
}
- else
+ finally
{
- 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";
+ isGenerating = false;
}
}
@@ -376,16 +192,29 @@ namespace AIImages
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()
- );
+ // Разделяем на две колонки: левая - информация, правая - изображение
+ float leftColumnWidth = inRect.width * 0.55f;
+ float rightColumnWidth = inRect.width * 0.42f;
+ float columnGap = inRect.width * 0.03f;
- Widgets.BeginScrollView(scrollRect, ref scrollPosition, scrollViewRect);
+ // Левая колонка - прокручиваемая информация
+ Rect leftColumnRect = new Rect(0f, curY, leftColumnWidth, inRect.height - curY);
+ DrawLeftColumn(leftColumnRect);
+
+ // Правая колонка - превью и управление
+ Rect rightColumnRect = new Rect(
+ leftColumnWidth + columnGap,
+ curY,
+ rightColumnWidth,
+ inRect.height - curY
+ );
+ DrawRightColumn(rightColumnRect);
+ }
+
+ private void DrawLeftColumn(Rect rect)
+ {
+ Rect scrollViewRect = new Rect(0f, 0f, rect.width - 20f, CalculateContentHeight());
+ Widgets.BeginScrollView(rect, ref scrollPosition, scrollViewRect);
float contentY = 0f;
@@ -398,7 +227,9 @@ namespace AIImages
contentY += 35f;
Text.Font = GameFont.Small;
- string appearanceText = GetAppearanceDescription();
+ string appearanceText = AIImagesMod.PawnDescriptionService.GetAppearanceDescription(
+ pawn
+ );
float appearanceHeight = Text.CalcHeight(appearanceText, scrollViewRect.width - 30f);
Widgets.Label(
new Rect(20f, contentY, scrollViewRect.width - 30f, appearanceHeight),
@@ -419,60 +250,133 @@ namespace AIImages
contentY += 35f;
Text.Font = GameFont.Small;
- string apparelText = GetApparelDescription();
+ string apparelText = AIImagesMod.PawnDescriptionService.GetApparelDescription(pawn);
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;
+ Widgets.EndScrollView();
+ }
- // Секция "Stable Diffusion Промпт"
+ private void DrawRightColumn(Rect rect)
+ {
+ float curY = 0f;
+
+ // Превью изображения
+ if (generatedImage != null)
+ {
+ float imageSize = Mathf.Min(rect.width, 400f);
+ Rect imageRect = new Rect(
+ (rect.width - imageSize) / 2f,
+ curY,
+ imageSize,
+ imageSize
+ );
+ GUI.DrawTexture(imageRect, generatedImage);
+ curY += imageSize + 10f;
+ }
+ else
+ {
+ // Placeholder для изображения
+ float placeholderSize = Mathf.Min(rect.width, 300f);
+ Rect placeholderRect = new Rect(
+ (rect.width - placeholderSize) / 2f,
+ curY,
+ placeholderSize,
+ placeholderSize
+ );
+ Widgets.DrawBoxSolid(placeholderRect, new Color(0.2f, 0.2f, 0.2f));
+ Text.Anchor = TextAnchor.MiddleCenter;
+ Widgets.Label(placeholderRect, "AIImages.Generation.NoImage".Translate());
+ Text.Anchor = TextAnchor.UpperLeft;
+ curY += placeholderSize + 10f;
+ }
+
+ // Статус генерации
+ if (!string.IsNullOrEmpty(generationStatus))
+ {
+ Text.Font = GameFont.Small;
+ float statusHeight = Text.CalcHeight(generationStatus, rect.width);
+ Widgets.Label(new Rect(0f, curY, rect.width, statusHeight), generationStatus);
+ curY += statusHeight + 10f;
+ }
+
+ // Кнопка генерации
+ Text.Font = GameFont.Small;
+ if (
+ Widgets.ButtonText(
+ new Rect(0f, curY, rect.width, 35f),
+ isGenerating
+ ? "AIImages.Generation.Generating".Translate()
+ : "AIImages.Generation.Generate".Translate()
+ ) && !isGenerating
+ )
+ {
+ _ = GenerateImage();
+ }
+ curY += 40f;
+
+ // Промпт секция
Text.Font = GameFont.Medium;
Widgets.Label(
- new Rect(10f, contentY, scrollViewRect.width - 20f, 30f),
+ new Rect(0f, curY, rect.width, 30f),
"AIImages.Prompt.SectionTitle".Translate()
);
- contentY += 35f;
+ curY += 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
+ // Получаем промпт
+ Text.Font = GameFont.Tiny;
+ string promptText = AIImagesMod.PromptGeneratorService.GeneratePositivePrompt(
+ appearanceData,
+ generationSettings
);
- contentY += promptHeight + 10f;
- // Кнопка копирования
- Rect copyButtonRect = new Rect(20f, contentY, 150f, 30f);
+ float promptHeight = Mathf.Min(Text.CalcHeight(promptText, rect.width), 150f);
+ Rect promptRect = new Rect(0f, curY, rect.width, promptHeight);
- if (Widgets.ButtonText(copyButtonRect, "AIImages.Prompt.CopyButton".Translate()))
+ // Рисуем промпт в скроллируемой области если он длинный
+ Widgets.DrawBoxSolid(promptRect, new Color(0.1f, 0.1f, 0.1f, 0.5f));
+ Widgets.Label(promptRect.ContractedBy(5f), promptText);
+ curY += promptHeight + 10f;
+
+ // Кнопка копирования промпта
+ if (
+ Widgets.ButtonText(
+ new Rect(0f, curY, rect.width / 2f - 5f, 30f),
+ "AIImages.Prompt.CopyButton".Translate()
+ )
+ )
{
GUIUtility.systemCopyBuffer = promptText;
- copiedMessageTime = 2f; // Показываем сообщение на 2 секунды
+ copiedMessageTime = 2f;
+ }
+
+ // Кнопка обновления данных
+ if (
+ Widgets.ButtonText(
+ new Rect(rect.width / 2f + 5f, curY, rect.width / 2f - 5f, 30f),
+ "AIImages.Window.Refresh".Translate()
+ )
+ )
+ {
+ RefreshPawnData();
}
// Сообщение о копировании
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());
+ curY += 35f;
+ GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f);
+ Widgets.Label(
+ new Rect(0f, curY, rect.width, 25f),
+ "AIImages.Prompt.Copied".Translate()
+ );
GUI.color = Color.white;
}
-
- Widgets.EndScrollView();
}
- ///
- /// Вычисляет высоту всего контента для прокрутки
- ///
private float CalculateContentHeight()
{
float height = 0f;
@@ -481,8 +385,10 @@ namespace AIImages
height += 35f;
// Текст внешности
- string appearanceText = GetAppearanceDescription();
- height += Text.CalcHeight(appearanceText, 640f) + 20f;
+ string appearanceText = AIImagesMod.PawnDescriptionService.GetAppearanceDescription(
+ pawn
+ );
+ height += Text.CalcHeight(appearanceText, 400f) + 20f;
// Разделитель
height += 15f;
@@ -491,21 +397,11 @@ namespace AIImages
height += 35f;
// Текст одежды
- string apparelText = GetApparelDescription();
- height += Text.CalcHeight(apparelText, 640f) + 20f;
+ string apparelText = AIImagesMod.PawnDescriptionService.GetApparelDescription(pawn);
+ height += Text.CalcHeight(apparelText, 400f) + 20f;
- // Разделитель
- height += 15f;
-
- // Заголовок "Промпт"
- height += 35f;
-
- // Текст промпта
- string promptText = GenerateStableDiffusionPrompt();
- height += Text.CalcHeight(promptText, 640f) + 10f;
-
- // Кнопка и отступ
- height += 30f + 20f;
+ // Дополнительный отступ
+ height += 50f;
return height;
}