All checks were successful
SonarQube Analysis / Build and analyze (push) Successful in 1m38s
1332 lines
50 KiB
C#
1332 lines
50 KiB
C#
using System;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using AIImages.Helpers;
|
||
using AIImages.Models;
|
||
using AIImages.Services;
|
||
using RimWorld;
|
||
using UnityEngine;
|
||
using Verse;
|
||
|
||
#pragma warning disable IDE1006 // Naming Styles
|
||
|
||
namespace AIImages
|
||
{
|
||
/// <summary>
|
||
/// Окно для просмотра персонажа и генерации AI изображений
|
||
/// </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;
|
||
private PawnAppearanceData appearanceData;
|
||
private StableDiffusionSettings generationSettings;
|
||
|
||
private Texture2D generatedImage;
|
||
private bool isGenerating = false;
|
||
private string generationStatus = "";
|
||
private CancellationTokenSource cancellationTokenSource;
|
||
|
||
// Прогресс генерации
|
||
private double generationProgress = 0.0;
|
||
private int currentStep = 0;
|
||
private int totalSteps = 0;
|
||
private double etaSeconds = 0.0;
|
||
|
||
// Сервисы (получаем через DI)
|
||
private readonly IPawnDescriptionService pawnDescriptionService;
|
||
private readonly IPromptGeneratorService promptGeneratorService;
|
||
private readonly IStableDiffusionApiService apiService;
|
||
|
||
public Window_AIImage(Pawn pawn)
|
||
{
|
||
this.pawn = pawn;
|
||
this.doCloseX = true;
|
||
this.doCloseButton = false; // Убираем дублирующую кнопку "Закрыть"
|
||
this.forcePause = false;
|
||
this.absorbInputAroundWindow = false;
|
||
this.draggable = true;
|
||
this.preventCameraMotion = false;
|
||
|
||
// Получаем сервисы через DI контейнер
|
||
var services = AIImagesMod.Services;
|
||
pawnDescriptionService = services.PawnDescriptionService;
|
||
promptGeneratorService = services.PromptGeneratorService;
|
||
apiService = services.ApiService;
|
||
|
||
// Извлекаем данные персонажа
|
||
RefreshPawnData();
|
||
}
|
||
|
||
public override Vector2 InitialSize => new Vector2(900f, 800f);
|
||
|
||
private Vector2 mainScrollPosition = Vector2.zero;
|
||
private Vector2 promptScrollPosition = Vector2.zero;
|
||
private Vector2 negativePromptScrollPosition = Vector2.zero;
|
||
private float copiedMessageTime = 0f;
|
||
|
||
// Состояние сворачиваемых секций промптов
|
||
private bool showPositivePrompt = false;
|
||
private bool showNegativePrompt = false;
|
||
|
||
/// <summary>
|
||
/// Обновляет данные персонажа
|
||
/// </summary>
|
||
private void RefreshPawnData()
|
||
{
|
||
DebugLogger.Log($"[AI Images] RefreshPawnData called for {pawn?.Name}");
|
||
|
||
appearanceData = pawnDescriptionService.ExtractAppearanceData(pawn);
|
||
generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings();
|
||
|
||
// Загружаем сохраненный портрет, если есть
|
||
LoadSavedPortrait();
|
||
|
||
DebugLogger.Log($"[AI Images] RefreshPawnData completed for {pawn?.Name}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Загружает сохраненный портрет персонажа
|
||
/// </summary>
|
||
private void LoadSavedPortrait()
|
||
{
|
||
DebugLogger.Log($"[AI Images] LoadSavedPortrait called for {pawn?.Name}");
|
||
|
||
if (PawnPortraitHelper.HasPortrait(pawn))
|
||
{
|
||
DebugLogger.Log($"[AI Images] Portrait found for {pawn?.Name}, loading...");
|
||
generatedImage = PawnPortraitHelper.LoadPortrait(pawn);
|
||
if (generatedImage != null)
|
||
{
|
||
generationStatus = "AIImages.Generation.LoadedFromSave".Translate();
|
||
DebugLogger.Log($"[AI Images] Successfully loaded portrait for {pawn?.Name}");
|
||
DebugLogger.Log(
|
||
$"[AI Images] generatedImage is now set: {generatedImage != null}, size: {generatedImage.width}x{generatedImage.height}"
|
||
);
|
||
}
|
||
else
|
||
{
|
||
DebugLogger.Warning(
|
||
$"[AI Images] Failed to load portrait texture for {pawn?.Name}"
|
||
);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
DebugLogger.Log($"[AI Images] No saved portrait found for {pawn?.Name}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Освобождает ресурсы при закрытии окна
|
||
/// </summary>
|
||
public override void PreClose()
|
||
{
|
||
base.PreClose();
|
||
|
||
// Отменяем генерацию, если она идет
|
||
if (isGenerating && cancellationTokenSource != null)
|
||
{
|
||
cancellationTokenSource.Cancel();
|
||
}
|
||
|
||
// Освобождаем CancellationTokenSource
|
||
cancellationTokenSource?.Dispose();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обновляет текущую пешку в окне
|
||
/// </summary>
|
||
public void UpdatePawn(Pawn newPawn)
|
||
{
|
||
DebugLogger.Log(
|
||
$"[AI Images] UpdatePawn called - switching from {pawn?.Name} to {newPawn?.Name}"
|
||
);
|
||
|
||
this.pawn = newPawn;
|
||
|
||
// Очищаем старое изображение при смене персонажа
|
||
generatedImage = null;
|
||
generationStatus = "";
|
||
generationProgress = 0.0;
|
||
currentStep = 0;
|
||
totalSteps = 0;
|
||
etaSeconds = 0.0;
|
||
|
||
DebugLogger.Log($"[AI Images] Cleared generated image for {newPawn?.Name}");
|
||
|
||
// Сбрасываем состояние сворачиваемых секций
|
||
showPositivePrompt = false;
|
||
showNegativePrompt = false;
|
||
|
||
// Обновляем данные персонажа (включая загрузку портрета)
|
||
RefreshPawnData();
|
||
|
||
// Принудительно обновляем окно
|
||
this.windowRect = new Rect(
|
||
this.windowRect.x,
|
||
this.windowRect.y,
|
||
this.InitialSize.x,
|
||
this.InitialSize.y
|
||
);
|
||
|
||
// Принудительно перерисовываем окно
|
||
this.SetInitialSizeAndPosition();
|
||
|
||
DebugLogger.Log($"[AI Images] UpdatePawn completed for {newPawn?.Name}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить текущую пешку
|
||
/// </summary>
|
||
public Pawn CurrentPawn => pawn;
|
||
|
||
/// <summary>
|
||
/// Отладочный метод для проверки состояния всех пешек
|
||
/// </summary>
|
||
public static void DebugAllPawns()
|
||
{
|
||
DebugLogger.Log("[AI Images] === DEBUG: Checking all pawns ===");
|
||
|
||
if (Current.Game?.Maps == null)
|
||
{
|
||
DebugLogger.Log("[AI Images] No game or maps available");
|
||
return;
|
||
}
|
||
|
||
int totalPawns = 0;
|
||
int pawnsWithComponents = 0;
|
||
int pawnsWithPortraits = 0;
|
||
|
||
foreach (var map in Current.Game.Maps)
|
||
{
|
||
foreach (var pawn in map.mapPawns.AllPawns)
|
||
{
|
||
if (pawn.RaceProps?.Humanlike == true)
|
||
{
|
||
totalPawns++;
|
||
var (hasComponent, hasPortrait) = CheckPawnPortraitStatus(pawn);
|
||
if (hasComponent)
|
||
pawnsWithComponents++;
|
||
if (hasPortrait)
|
||
pawnsWithPortraits++;
|
||
}
|
||
}
|
||
}
|
||
|
||
LogDebugSummary(totalPawns, pawnsWithComponents, pawnsWithPortraits);
|
||
}
|
||
|
||
private static (bool hasComponent, bool hasPortrait) CheckPawnPortraitStatus(Pawn pawn)
|
||
{
|
||
var comp = PawnPortraitHelper.GetPortraitComp(pawn);
|
||
if (comp != null)
|
||
{
|
||
if (comp.HasPortrait)
|
||
{
|
||
DebugLogger.Log(
|
||
$"[AI Images] {pawn.Name}: Has component with portrait '{comp.PortraitPath}'"
|
||
);
|
||
return (true, true);
|
||
}
|
||
else
|
||
{
|
||
DebugLogger.Log($"[AI Images] {pawn.Name}: Has component but no portrait");
|
||
return (true, false);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
DebugLogger.Warning($"[AI Images] {pawn.Name}: No portrait component found!");
|
||
return (false, false);
|
||
}
|
||
}
|
||
|
||
private static void LogDebugSummary(
|
||
int totalPawns,
|
||
int pawnsWithComponents,
|
||
int pawnsWithPortraits
|
||
)
|
||
{
|
||
DebugLogger.Log($"[AI Images] === DEBUG SUMMARY ===");
|
||
DebugLogger.Log($"[AI Images] Total humanlike pawns: {totalPawns}");
|
||
DebugLogger.Log($"[AI Images] Pawns with components: {pawnsWithComponents}");
|
||
DebugLogger.Log($"[AI Images] Pawns with portraits: {pawnsWithPortraits}");
|
||
DebugLogger.Log($"[AI Images] === END DEBUG ===");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Вызывается каждый кадр для обновления окна
|
||
/// </summary>
|
||
public override void WindowUpdate()
|
||
{
|
||
base.WindowUpdate();
|
||
|
||
// Проверяем, изменилась ли выбранная пешка
|
||
Pawn selectedPawn = Find.Selector.SelectedPawns.FirstOrDefault(p => p.Spawned);
|
||
|
||
// Если выбрана новая пешка, обновляем окно
|
||
if (selectedPawn != null && selectedPawn != pawn)
|
||
{
|
||
UpdatePawn(selectedPawn);
|
||
}
|
||
|
||
// Уменьшаем таймер сообщения о копировании
|
||
if (copiedMessageTime > 0f)
|
||
{
|
||
copiedMessageTime -= Time.deltaTime;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Асинхронная генерация изображения с поддержкой отмены
|
||
/// </summary>
|
||
private async System.Threading.Tasks.Task GenerateImageAsync()
|
||
{
|
||
if (isGenerating)
|
||
return;
|
||
|
||
// Создаем новый CancellationTokenSource
|
||
cancellationTokenSource?.Dispose();
|
||
cancellationTokenSource = new CancellationTokenSource();
|
||
|
||
isGenerating = true;
|
||
generationStatus = "AIImages.Generation.InProgress".Translate();
|
||
generationProgress = 0.0;
|
||
currentStep = 0;
|
||
totalSteps = generationSettings.Steps;
|
||
|
||
try
|
||
{
|
||
// Генерируем промпты
|
||
string positivePrompt = promptGeneratorService.GeneratePositivePrompt(
|
||
appearanceData,
|
||
generationSettings
|
||
);
|
||
string negativePrompt = promptGeneratorService.GenerateNegativePrompt(
|
||
generationSettings
|
||
);
|
||
|
||
// Создаем запрос
|
||
var request = new GenerationRequest
|
||
{
|
||
Prompt = positivePrompt,
|
||
NegativePrompt = negativePrompt,
|
||
Steps = generationSettings.Steps,
|
||
CfgScale = generationSettings.CfgScale,
|
||
Width = generationSettings.Width,
|
||
Height = generationSettings.Height,
|
||
Sampler = generationSettings.Sampler,
|
||
Scheduler = generationSettings.Scheduler,
|
||
Seed = generationSettings.Seed,
|
||
Model = AIImagesMod.Settings.apiEndpoint,
|
||
SaveImagesToServer = AIImagesMod.Settings.saveImagesToServer,
|
||
};
|
||
|
||
// Создаем отдельный CancellationTokenSource для мониторинга прогресса
|
||
var progressCts = new CancellationTokenSource();
|
||
var progressTask = MonitorProgressAsync(progressCts.Token);
|
||
|
||
// Генерируем изображение с поддержкой отмены
|
||
var result = await apiService.GenerateImageAsync(
|
||
request,
|
||
cancellationTokenSource.Token
|
||
);
|
||
|
||
// Останавливаем мониторинг прогресса
|
||
progressCts.Cancel();
|
||
try
|
||
{
|
||
await progressTask;
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// Ожидаемое исключение при остановке мониторинга
|
||
}
|
||
finally
|
||
{
|
||
progressCts.Dispose();
|
||
}
|
||
|
||
if (result.Success)
|
||
{
|
||
DebugLogger.Log($"[AI Images] Generation successful for {pawn?.Name}");
|
||
DebugLogger.Log(
|
||
$"[AI Images] Image data size: {result.ImageData?.Length ?? 0} bytes"
|
||
);
|
||
DebugLogger.Log($"[AI Images] Saved path: {result.SavedPath}");
|
||
|
||
// Загружаем текстуру
|
||
generatedImage = new Texture2D(2, 2);
|
||
generatedImage.LoadImage(result.ImageData);
|
||
generationStatus = "AIImages.Generation.Success".Translate();
|
||
generationProgress = 1.0;
|
||
|
||
// Сохраняем путь к портрету на персонаже
|
||
DebugLogger.Log(
|
||
$"[AI Images] About to save portrait path for {pawn?.Name}: {result.SavedPath}"
|
||
);
|
||
PawnPortraitHelper.SavePortraitPath(pawn, result.SavedPath);
|
||
|
||
Messages.Message(
|
||
"AIImages.Generation.SavedTo".Translate(result.SavedPath),
|
||
MessageTypeDefOf.PositiveEvent
|
||
);
|
||
|
||
DebugLogger.Log(
|
||
$"[AI Images] Portrait generation and saving completed for {pawn?.Name}"
|
||
);
|
||
}
|
||
else
|
||
{
|
||
generationStatus =
|
||
$"AIImages.Generation.Failed".Translate() + ": {result.ErrorMessage}";
|
||
Messages.Message(generationStatus, MessageTypeDefOf.RejectInput);
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
generationStatus = "AIImages.Generation.Cancelled".Translate();
|
||
DebugLogger.Log("[AI Images] Generation cancelled by user");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
generationStatus = $"Error: {ex.Message}";
|
||
DebugLogger.Error($"[AI Images] Generation error: {ex}");
|
||
Messages.Message(
|
||
$"AIImages.Generation.Error".Translate() + ": {ex.Message}",
|
||
MessageTypeDefOf.RejectInput
|
||
);
|
||
}
|
||
finally
|
||
{
|
||
isGenerating = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Мониторит прогресс генерации и обновляет UI
|
||
/// </summary>
|
||
private async System.Threading.Tasks.Task MonitorProgressAsync(
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
try
|
||
{
|
||
while (!cancellationToken.IsCancellationRequested)
|
||
{
|
||
var progress = await apiService.GetProgressAsync(cancellationToken);
|
||
|
||
if (progress != null && progress.IsActive)
|
||
{
|
||
generationProgress = progress.Progress;
|
||
currentStep = progress.CurrentStep;
|
||
totalSteps = progress.TotalSteps;
|
||
etaSeconds = progress.EtaRelative;
|
||
|
||
DebugLogger.Log(
|
||
$"[AI Images] Progress: {progress.Progress:P} - Step {progress.CurrentStep}/{progress.TotalSteps} - ETA: {progress.EtaRelative:F1}s"
|
||
);
|
||
}
|
||
|
||
// Обновляем каждые 500ms
|
||
await System.Threading.Tasks.Task.Delay(500, cancellationToken);
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// Ожидаемое исключение при остановке
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DebugLogger.Warning($"[AI Images] Progress monitoring error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Запускает генерацию изображения (обертка для безопасного fire-and-forget)
|
||
/// </summary>
|
||
private void StartGeneration()
|
||
{
|
||
AsyncHelper.FireAndForget(GenerateImageAsync(), "Image Generation");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отменяет генерацию изображения
|
||
/// </summary>
|
||
private void CancelGeneration()
|
||
{
|
||
if (isGenerating && cancellationTokenSource != null)
|
||
{
|
||
cancellationTokenSource.Cancel();
|
||
generationStatus = "AIImages.Generation.Cancelling".Translate();
|
||
}
|
||
}
|
||
|
||
public override void DoWindowContents(Rect inRect)
|
||
{
|
||
// Рассчитываем общую высоту контента
|
||
float totalContentHeight = CalculateTotalContentHeight();
|
||
Rect viewRect = new Rect(0f, 0f, inRect.width - 20f, totalContentHeight);
|
||
|
||
Widgets.BeginScrollView(inRect, ref mainScrollPosition, viewRect);
|
||
|
||
float curY = 0f;
|
||
|
||
// Заголовок
|
||
Text.Font = GameFont.Medium;
|
||
Widgets.Label(
|
||
new Rect(0f, curY, viewRect.width, 40f),
|
||
"AIImages.Window.Title".Translate()
|
||
);
|
||
curY += 45f;
|
||
|
||
// Имя пешки
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(
|
||
new Rect(0f, curY, viewRect.width, 30f),
|
||
"AIImages.Window.PawnLabel".Translate(pawn.NameShortColored.Resolve())
|
||
);
|
||
curY += 40f;
|
||
|
||
// Разделитель
|
||
Widgets.DrawLineHorizontal(0f, curY, viewRect.width);
|
||
curY += 10f;
|
||
|
||
// Разделяем на две колонки: левая - информация, правая - изображение
|
||
float leftColumnWidth = viewRect.width * 0.35f;
|
||
float rightColumnWidth = viewRect.width * 0.62f;
|
||
float columnGap = viewRect.width * 0.03f;
|
||
|
||
// Определяем высоту колонок (берем большую из двух)
|
||
float columnHeight = Mathf.Max(CalculateContentHeight(), CalculateRightColumnHeight());
|
||
|
||
// Левая колонка - информация
|
||
Rect leftColumnRect = new Rect(0f, curY, leftColumnWidth, columnHeight);
|
||
DrawLeftColumn(leftColumnRect);
|
||
|
||
// Правая колонка - превью и управление
|
||
Rect rightColumnRect = new Rect(
|
||
leftColumnWidth + columnGap,
|
||
curY,
|
||
rightColumnWidth,
|
||
columnHeight
|
||
);
|
||
DrawRightColumn(rightColumnRect);
|
||
|
||
Widgets.EndScrollView();
|
||
}
|
||
|
||
private void DrawLeftColumn(Rect rect)
|
||
{
|
||
float contentY = rect.y;
|
||
|
||
// Портрет персонажа (если есть)
|
||
if (generatedImage != null)
|
||
{
|
||
float portraitSize = Mathf.Min(rect.width, 250f);
|
||
Rect portraitRect = new Rect(
|
||
rect.x + (rect.width - portraitSize) / 2f,
|
||
contentY,
|
||
portraitSize,
|
||
portraitSize
|
||
);
|
||
|
||
// Рамка вокруг портрета
|
||
Widgets.DrawBox(portraitRect);
|
||
GUI.DrawTexture(portraitRect.ContractedBy(2f), generatedImage);
|
||
contentY += portraitSize + 15f;
|
||
}
|
||
|
||
// Заголовок секции
|
||
Text.Font = GameFont.Medium;
|
||
Widgets.Label(
|
||
new Rect(rect.x, contentY, rect.width, 30f),
|
||
"AIImages.CharacterInfo.Title".Translate()
|
||
);
|
||
contentY += 35f;
|
||
|
||
// Разделитель
|
||
Widgets.DrawLineHorizontal(rect.x, contentY, rect.width);
|
||
contentY += 10f;
|
||
|
||
// Информация о персонаже
|
||
DrawCharacterInfoContent(rect, contentY);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отрисовывает информацию о персонаже в компактном виде
|
||
/// </summary>
|
||
private void DrawCharacterInfoContent(Rect parentRect, float startY)
|
||
{
|
||
float contentY = startY;
|
||
float lineHeight = 22f;
|
||
|
||
contentY = DrawBasicInfo(parentRect, contentY, lineHeight);
|
||
contentY = DrawTraits(parentRect, contentY, lineHeight);
|
||
contentY = DrawGenes(parentRect, contentY, lineHeight);
|
||
contentY = DrawHediffs(parentRect, contentY, lineHeight);
|
||
DrawApparel(parentRect, contentY, lineHeight);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отрисовывает базовую информацию о персонаже
|
||
/// </summary>
|
||
private float DrawBasicInfo(Rect parentRect, float startY, float lineHeight)
|
||
{
|
||
float contentY = startY;
|
||
float labelWidth = parentRect.width * 0.45f;
|
||
float valueWidth = parentRect.width * 0.50f;
|
||
|
||
Text.Font = GameFont.Tiny;
|
||
|
||
var info = new[]
|
||
{
|
||
("AIImages.Info.Gender".Translate(), appearanceData.Gender.ToString()),
|
||
("AIImages.Info.Age".Translate(), appearanceData.Age.ToString()),
|
||
("AIImages.Info.BodyType".Translate(), appearanceData.BodyType),
|
||
(
|
||
"AIImages.Info.SkinTone".Translate(),
|
||
ColorDescriptionService.GetSkinToneDescription(pawn)
|
||
),
|
||
("AIImages.Info.Hair".Translate(), appearanceData.HairStyle),
|
||
(
|
||
"AIImages.Info.HairColor".Translate(),
|
||
ColorDescriptionService.GetHairColorDescription(pawn)
|
||
),
|
||
};
|
||
|
||
foreach (var (label, value) in info)
|
||
{
|
||
// Подсветка строк через одну
|
||
var infoArray = info.ToArray();
|
||
int index = Array.IndexOf(infoArray, (label, value));
|
||
if ((index % 2) == 0)
|
||
{
|
||
Widgets.DrawBoxSolid(
|
||
new Rect(parentRect.x, contentY, parentRect.width, lineHeight),
|
||
new Color(0.15f, 0.15f, 0.15f, 0.3f)
|
||
);
|
||
}
|
||
|
||
Widgets.Label(
|
||
new Rect(parentRect.x + 5f, contentY, labelWidth, lineHeight),
|
||
label + ":"
|
||
);
|
||
Widgets.Label(
|
||
new Rect(parentRect.x + labelWidth + 10f, contentY, valueWidth, lineHeight),
|
||
value
|
||
);
|
||
contentY += lineHeight;
|
||
}
|
||
|
||
return contentY;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отрисовывает черты характера персонажа
|
||
/// </summary>
|
||
private float DrawTraits(Rect parentRect, float startY, float lineHeight)
|
||
{
|
||
float contentY = startY;
|
||
|
||
if (pawn.story?.traits?.allTraits == null || !pawn.story.traits.allTraits.Any())
|
||
return contentY;
|
||
|
||
contentY += 15f;
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(
|
||
new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight),
|
||
"AIImages.Info.Traits".Translate() + ":"
|
||
);
|
||
contentY += lineHeight + 2f;
|
||
|
||
Text.Font = GameFont.Tiny;
|
||
var traitLabels = pawn.story.traits.allTraits.Select(trait => trait.LabelCap);
|
||
foreach (var traitLabel in traitLabels)
|
||
{
|
||
Widgets.Label(
|
||
new Rect(parentRect.x + 15f, contentY, parentRect.width - 20f, lineHeight),
|
||
"• " + traitLabel
|
||
);
|
||
contentY += lineHeight;
|
||
}
|
||
|
||
return contentY;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отрисовывает гены персонажа
|
||
/// </summary>
|
||
private float DrawGenes(Rect parentRect, float startY, float lineHeight)
|
||
{
|
||
float contentY = startY;
|
||
|
||
if (pawn.genes?.GenesListForReading == null || !pawn.genes.GenesListForReading.Any())
|
||
return contentY;
|
||
|
||
contentY += 15f;
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(
|
||
new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight),
|
||
"AIImages.Info.Genes".Translate() + ":"
|
||
);
|
||
contentY += lineHeight + 2f;
|
||
|
||
Text.Font = GameFont.Tiny;
|
||
var activeGenes = pawn.genes.GenesListForReading.Where(gene => gene.Active).ToList();
|
||
foreach (var gene in activeGenes)
|
||
{
|
||
string geneInfo = BuildGeneInfo(gene);
|
||
float textHeight = Text.CalcHeight(geneInfo, parentRect.width - 25f);
|
||
|
||
Widgets.Label(
|
||
new Rect(parentRect.x + 15f, contentY, parentRect.width - 25f, textHeight),
|
||
"• " + geneInfo
|
||
);
|
||
contentY += textHeight;
|
||
}
|
||
|
||
return contentY;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Строит информационную строку для гена
|
||
/// </summary>
|
||
private string BuildGeneInfo(Gene gene)
|
||
{
|
||
string geneLabel = gene.Label ?? gene.def.LabelCap;
|
||
string geneInfo = geneLabel;
|
||
|
||
if (!string.IsNullOrEmpty(gene.def.description))
|
||
{
|
||
string description = gene.def.description;
|
||
if (description.Length > 200)
|
||
{
|
||
description = description.Substring(0, 197) + "...";
|
||
}
|
||
geneInfo += " - " + description;
|
||
}
|
||
|
||
return geneInfo;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отрисовывает хедифы (состояния здоровья) персонажа
|
||
/// </summary>
|
||
private float DrawHediffs(Rect parentRect, float startY, float lineHeight)
|
||
{
|
||
float contentY = startY;
|
||
|
||
if (pawn.health?.hediffSet?.hediffs == null || !pawn.health.hediffSet.hediffs.Any())
|
||
return contentY;
|
||
|
||
contentY += 15f;
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(
|
||
new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight),
|
||
"AIImages.Info.Hediffs".Translate() + ":"
|
||
);
|
||
contentY += lineHeight + 2f;
|
||
|
||
Text.Font = GameFont.Tiny;
|
||
var hediffLabels = pawn
|
||
.health.hediffSet.hediffs.Where(hediff => hediff.Visible)
|
||
.Select(hediff => hediff.LabelCap);
|
||
foreach (var hediffLabel in hediffLabels)
|
||
{
|
||
Widgets.Label(
|
||
new Rect(parentRect.x + 15f, contentY, parentRect.width - 20f, lineHeight),
|
||
"• " + hediffLabel
|
||
);
|
||
contentY += lineHeight;
|
||
}
|
||
|
||
return contentY;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отрисовывает одежду персонажа
|
||
/// </summary>
|
||
private void DrawApparel(Rect parentRect, float startY, float lineHeight)
|
||
{
|
||
float contentY = startY;
|
||
var apparel = pawn.apparel?.WornApparel;
|
||
|
||
if (apparel == null || !apparel.Any())
|
||
return;
|
||
|
||
contentY += 15f;
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(
|
||
new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight),
|
||
"AIImages.Info.Apparel".Translate() + ":"
|
||
);
|
||
contentY += lineHeight + 2f;
|
||
|
||
Text.Font = GameFont.Tiny;
|
||
foreach (var item in apparel)
|
||
{
|
||
var colorDesc = ColorDescriptionService.GetApparelColorDescription(item.DrawColor);
|
||
string apparelLabel = $"• {colorDesc} {item.def.label}";
|
||
float apparelHeight = Text.CalcHeight(apparelLabel, parentRect.width - 25f);
|
||
Widgets.Label(
|
||
new Rect(parentRect.x + 15f, contentY, parentRect.width - 25f, apparelHeight),
|
||
apparelLabel
|
||
);
|
||
contentY += apparelHeight;
|
||
}
|
||
}
|
||
|
||
private void DrawRightColumn(Rect rect)
|
||
{
|
||
float curY = rect.y;
|
||
|
||
curY = DrawImageTypeSelector(rect, curY);
|
||
curY = DrawImagePreview(rect, curY);
|
||
curY = DrawGenerationStatus(rect, curY);
|
||
curY = DrawProgressBar(rect, curY);
|
||
curY = DrawGenerationButton(rect, curY);
|
||
|
||
// Сворачиваемая секция с позитивным промптом
|
||
curY = DrawPromptSection(
|
||
rect,
|
||
curY,
|
||
"AIImages.Prompt.PositiveTitle".Translate(),
|
||
ref showPositivePrompt,
|
||
() =>
|
||
promptGeneratorService.GeneratePositivePrompt(
|
||
appearanceData,
|
||
generationSettings
|
||
),
|
||
new Color(0.1f, 0.3f, 0.1f, 0.5f),
|
||
ref promptScrollPosition
|
||
);
|
||
|
||
curY += 5f;
|
||
|
||
// Сворачиваемая секция с негативным промптом
|
||
curY = DrawPromptSection(
|
||
rect,
|
||
curY,
|
||
"AIImages.Prompt.NegativeTitle".Translate(),
|
||
ref showNegativePrompt,
|
||
() => promptGeneratorService.GenerateNegativePrompt(generationSettings),
|
||
new Color(0.3f, 0.1f, 0.1f, 0.5f),
|
||
ref negativePromptScrollPosition
|
||
);
|
||
|
||
curY += 10f;
|
||
|
||
// Кнопка обновления данных
|
||
if (
|
||
Widgets.ButtonText(
|
||
new Rect(rect.x, curY, rect.width, 30f),
|
||
"AIImages.Window.Refresh".Translate()
|
||
)
|
||
)
|
||
{
|
||
RefreshPawnData();
|
||
}
|
||
|
||
// Сообщение о копировании
|
||
if (copiedMessageTime > 0f)
|
||
{
|
||
curY += 35f;
|
||
GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f);
|
||
Widgets.Label(
|
||
new Rect(rect.x, curY, rect.width, 25f),
|
||
"AIImages.Prompt.Copied".Translate()
|
||
);
|
||
GUI.color = Color.white;
|
||
}
|
||
}
|
||
|
||
private float CalculateContentHeight()
|
||
{
|
||
float height = CalculatePortraitHeight();
|
||
height += CalculateBasicInfoHeight();
|
||
height += CalculateTraitsHeight();
|
||
height += CalculateGenesHeight();
|
||
height += CalculateHediffsHeight();
|
||
height += CalculateApparelHeight();
|
||
height += 50f; // Дополнительный отступ
|
||
return height;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Рассчитывает высоту портрета персонажа
|
||
/// </summary>
|
||
private float CalculatePortraitHeight()
|
||
{
|
||
if (generatedImage != null)
|
||
{
|
||
float portraitSize = 250f; // Максимальный размер портрета
|
||
return portraitSize + 15f;
|
||
}
|
||
return 0f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Рассчитывает высоту базовой информации
|
||
/// </summary>
|
||
private float CalculateBasicInfoHeight()
|
||
{
|
||
float height = 35f; // Заголовок "Информация о персонаже"
|
||
height += 10f; // Разделитель
|
||
height += 6 * 22f; // Базовая информация (6 строк по 22px)
|
||
return height;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Рассчитывает высоту секции черт характера
|
||
/// </summary>
|
||
private float CalculateTraitsHeight()
|
||
{
|
||
if (pawn.story?.traits?.allTraits == null || !pawn.story.traits.allTraits.Any())
|
||
return 0f;
|
||
|
||
float height = 15f; // Отступ
|
||
height += 22f; // Заголовок "Черты характера"
|
||
height += 2f; // Отступ
|
||
height += pawn.story.traits.allTraits.Count * 22f; // Каждая черта
|
||
return height;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Рассчитывает высоту секции генов
|
||
/// </summary>
|
||
private float CalculateGenesHeight()
|
||
{
|
||
if (pawn.genes?.GenesListForReading == null || !pawn.genes.GenesListForReading.Any())
|
||
return 0f;
|
||
|
||
float height = 15f; // Отступ
|
||
height += 22f; // Заголовок "Гены"
|
||
height += 2f; // Отступ
|
||
|
||
// Рассчитываем высоту для каждого гена с учетом описания
|
||
Text.Font = GameFont.Tiny;
|
||
var activeGenes = pawn.genes.GenesListForReading.Where(gene => gene.Active).ToList();
|
||
float approximateColumnWidth = 900f * 0.35f; // Примерная ширина левой колонки
|
||
float labelWidth = approximateColumnWidth - 25f;
|
||
|
||
foreach (var gene in activeGenes)
|
||
{
|
||
string geneInfo = BuildGeneInfo(gene);
|
||
height += Text.CalcHeight("• " + geneInfo, labelWidth);
|
||
}
|
||
|
||
return height;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Рассчитывает высоту секции хедифов
|
||
/// </summary>
|
||
private float CalculateHediffsHeight()
|
||
{
|
||
if (pawn.health?.hediffSet?.hediffs == null || !pawn.health.hediffSet.hediffs.Any())
|
||
return 0f;
|
||
|
||
float height = 15f; // Отступ
|
||
height += 22f; // Заголовок "Хедифы"
|
||
height += 2f; // Отступ
|
||
height += pawn.health.hediffSet.hediffs.Count(hediff => hediff.Visible) * 22f; // Каждый видимый хедиф
|
||
return height;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Рассчитывает высоту секции одежды
|
||
/// </summary>
|
||
private float CalculateApparelHeight()
|
||
{
|
||
var apparel = pawn.apparel?.WornApparel;
|
||
if (apparel == null || !apparel.Any())
|
||
return 0f;
|
||
|
||
float height = 15f; // Отступ
|
||
height += 22f; // Заголовок "Одежда"
|
||
height += 2f; // Отступ
|
||
height += apparel.Count * 26f; // Примерно по 22-30px на предмет одежды
|
||
return height;
|
||
}
|
||
|
||
private float CalculateRightColumnHeight()
|
||
{
|
||
float height = 0f;
|
||
|
||
// Превью изображения
|
||
if (generatedImage != null)
|
||
{
|
||
height += 410f; // 400f изображение + 10f отступ
|
||
}
|
||
else if (!isGenerating)
|
||
{
|
||
height += 310f; // 300f placeholder + 10f отступ
|
||
}
|
||
|
||
// Статус генерации
|
||
if (!string.IsNullOrEmpty(generationStatus))
|
||
{
|
||
height += 30f;
|
||
}
|
||
|
||
// Прогресс бар
|
||
if (isGenerating && generationProgress > 0.0)
|
||
{
|
||
height += 30f;
|
||
}
|
||
|
||
// Кнопка генерации
|
||
height += 40f;
|
||
|
||
// Позитивный промпт
|
||
height += 36f; // Заголовок (32f + 4f отступ)
|
||
if (showPositivePrompt)
|
||
{
|
||
height += 140f; // Бокс развернут (120f + 10f отступ + запас)
|
||
}
|
||
height += 5f; // Отступ между промптами
|
||
|
||
// Негативный промпт
|
||
height += 36f; // Заголовок (32f + 4f отступ)
|
||
if (showNegativePrompt)
|
||
{
|
||
height += 140f; // Бокс развернут (120f + 10f отступ + запас)
|
||
}
|
||
height += 10f; // Отступ после промптов
|
||
|
||
// Кнопка обновления
|
||
height += 40f; // Увеличено с 35f до 40f
|
||
|
||
// Сообщение о копировании
|
||
if (copiedMessageTime > 0f)
|
||
{
|
||
height += 40f;
|
||
}
|
||
|
||
return height + 100f; // Увеличен дополнительный отступ снизу
|
||
}
|
||
|
||
private float CalculateTotalContentHeight()
|
||
{
|
||
float height = 0f;
|
||
|
||
// Заголовок и имя пешки
|
||
height += 45f + 40f;
|
||
|
||
// Разделитель
|
||
height += 10f;
|
||
|
||
// Высота колонок (берем большую)
|
||
float columnHeight = Mathf.Max(CalculateContentHeight(), CalculateRightColumnHeight());
|
||
height += columnHeight;
|
||
|
||
// Дополнительный отступ снизу
|
||
height += 30f;
|
||
|
||
return height;
|
||
}
|
||
|
||
/// <summary>
|
||
/// /// Отрисовывает секцию с промптом
|
||
/// </summary>
|
||
private float DrawPromptSection(
|
||
Rect parentRect,
|
||
float startY,
|
||
string title,
|
||
ref bool expanded,
|
||
System.Func<string> getPromptFunc,
|
||
Color backgroundColor,
|
||
ref Vector2 scrollPosition
|
||
)
|
||
{
|
||
float curY = startY;
|
||
float headerHeight = 32f;
|
||
|
||
// Получаем промпт
|
||
string prompt = getPromptFunc();
|
||
|
||
// Рисуем заголовок с фоном
|
||
Rect headerRect = new Rect(parentRect.x, curY, parentRect.width, headerHeight);
|
||
Widgets.DrawBoxSolid(headerRect, new Color(0.25f, 0.25f, 0.25f, 0.8f));
|
||
Widgets.DrawBox(headerRect);
|
||
|
||
// Иконка раскрытия
|
||
string icon = expanded ? "▼" : "►";
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(new Rect(parentRect.x + 8f, curY + 6f, 20f, headerHeight), icon);
|
||
|
||
// Заголовок
|
||
Widgets.Label(
|
||
new Rect(parentRect.x + 30f, curY + 6f, parentRect.width - 110f, headerHeight),
|
||
title
|
||
);
|
||
|
||
// Кнопка копирования в заголовке (увеличена ширина)
|
||
Rect copyButtonRect = new Rect(
|
||
parentRect.x + parentRect.width - 100f,
|
||
curY + 4f,
|
||
95f,
|
||
24f
|
||
);
|
||
if (Widgets.ButtonText(copyButtonRect, "📋 " + "AIImages.Copy".Translate()))
|
||
{
|
||
GUIUtility.systemCopyBuffer = prompt;
|
||
copiedMessageTime = 2f;
|
||
}
|
||
|
||
// Клик на остальной области для раскрытия/сворачивания
|
||
Rect clickableHeaderRect = new Rect(
|
||
parentRect.x,
|
||
curY,
|
||
parentRect.width - 105f,
|
||
headerHeight
|
||
);
|
||
if (Widgets.ButtonInvisible(clickableHeaderRect))
|
||
{
|
||
expanded = !expanded;
|
||
}
|
||
|
||
curY += headerHeight + 4f;
|
||
|
||
// Рисуем содержимое если развернуто
|
||
if (expanded)
|
||
{
|
||
float promptBoxHeight = 120f;
|
||
Text.Font = GameFont.Tiny;
|
||
float actualPromptHeight = Text.CalcHeight(prompt, parentRect.width - 20f);
|
||
|
||
Rect promptOuterRect = new Rect(
|
||
parentRect.x,
|
||
curY,
|
||
parentRect.width,
|
||
promptBoxHeight
|
||
);
|
||
Rect promptViewRect = new Rect(0f, 0f, parentRect.width - 20f, actualPromptHeight);
|
||
|
||
// Рисуем фон
|
||
Widgets.DrawBoxSolid(promptOuterRect, backgroundColor);
|
||
Widgets.DrawBox(promptOuterRect);
|
||
|
||
// Рисуем промпт с прокруткой
|
||
Widgets.BeginScrollView(
|
||
promptOuterRect.ContractedBy(5f),
|
||
ref scrollPosition,
|
||
promptViewRect
|
||
);
|
||
|
||
var prevAnchor = Text.Anchor;
|
||
Text.Anchor = TextAnchor.UpperLeft;
|
||
Text.WordWrap = true;
|
||
|
||
Widgets.Label(
|
||
new Rect(0f, 0f, promptViewRect.width, promptViewRect.height),
|
||
prompt
|
||
);
|
||
|
||
Text.Anchor = prevAnchor;
|
||
Text.WordWrap = true;
|
||
|
||
Widgets.EndScrollView();
|
||
|
||
curY += promptBoxHeight + 10f;
|
||
}
|
||
|
||
return curY;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отрисовывает селектор типа изображения (портрет/полное тело)
|
||
/// </summary>
|
||
private float DrawImageTypeSelector(Rect rect, float curY)
|
||
{
|
||
Text.Font = GameFont.Small;
|
||
float selectorHeight = 30f;
|
||
|
||
// Заголовок
|
||
Widgets.Label(
|
||
new Rect(rect.x, curY, rect.width * 0.4f, selectorHeight),
|
||
"AIImages.ImageType.Label".Translate() + ":"
|
||
);
|
||
|
||
// Радио-кнопки для выбора типа
|
||
float radioX = rect.x + rect.width * 0.45f;
|
||
float radioWidth = rect.width * 0.25f;
|
||
float radioSpacing = 5f;
|
||
|
||
// Портрет
|
||
Rect portraitRect = new Rect(radioX, curY + 2f, radioWidth, selectorHeight - 4f);
|
||
bool isPortrait = generationSettings.ImageType == Models.ImageType.Portrait;
|
||
if (
|
||
Widgets.RadioButtonLabeled(
|
||
portraitRect,
|
||
"AIImages.ImageType.Portrait".Translate(),
|
||
isPortrait
|
||
) && !isPortrait
|
||
)
|
||
{
|
||
generationSettings.ImageType = Models.ImageType.Portrait;
|
||
// Обновляем настройки в глобальных настройках
|
||
AIImagesMod.Settings.imageType = Models.ImageType.Portrait;
|
||
// При смене на портрет, можно изменить размеры на более квадратные
|
||
if (generationSettings.Width > generationSettings.Height)
|
||
{
|
||
generationSettings.Height = generationSettings.Width;
|
||
}
|
||
}
|
||
|
||
// Полное тело
|
||
Rect fullBodyRect = new Rect(
|
||
radioX + radioWidth + radioSpacing,
|
||
curY + 2f,
|
||
radioWidth,
|
||
selectorHeight - 4f
|
||
);
|
||
bool isFullBody = generationSettings.ImageType == Models.ImageType.FullBody;
|
||
if (
|
||
Widgets.RadioButtonLabeled(
|
||
fullBodyRect,
|
||
"AIImages.ImageType.FullBody".Translate(),
|
||
isFullBody
|
||
) && !isFullBody
|
||
)
|
||
{
|
||
generationSettings.ImageType = Models.ImageType.FullBody;
|
||
// Обновляем настройки в глобальных настройках
|
||
AIImagesMod.Settings.imageType = Models.ImageType.FullBody;
|
||
// При смене на полное тело, можно увеличить высоту
|
||
if (generationSettings.Width >= generationSettings.Height)
|
||
{
|
||
generationSettings.Height = (int)(generationSettings.Width * 1.5f);
|
||
}
|
||
}
|
||
|
||
return curY + selectorHeight + 10f;
|
||
}
|
||
|
||
private float DrawImagePreview(Rect rect, float curY)
|
||
{
|
||
if (generatedImage != null)
|
||
{
|
||
float imageSize = Mathf.Min(rect.width, 400f);
|
||
Rect imageRect = new Rect(
|
||
rect.x + (rect.width - imageSize) / 2f,
|
||
curY,
|
||
imageSize,
|
||
imageSize
|
||
);
|
||
GUI.DrawTexture(imageRect, generatedImage);
|
||
return curY + imageSize + 10f;
|
||
}
|
||
|
||
float placeholderSize = Mathf.Min(rect.width, 300f);
|
||
Rect placeholderRect = new Rect(
|
||
rect.x + (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;
|
||
return curY + placeholderSize + 10f;
|
||
}
|
||
|
||
private float DrawGenerationStatus(Rect rect, float curY)
|
||
{
|
||
if (string.IsNullOrEmpty(generationStatus))
|
||
return curY;
|
||
|
||
Text.Font = GameFont.Small;
|
||
float statusHeight = Text.CalcHeight(generationStatus, rect.width);
|
||
Widgets.Label(new Rect(rect.x, curY, rect.width, statusHeight), generationStatus);
|
||
return curY + statusHeight + 10f;
|
||
}
|
||
|
||
private float DrawProgressBar(Rect rect, float curY)
|
||
{
|
||
if (!isGenerating || generationProgress <= 0.0)
|
||
return curY;
|
||
|
||
Rect progressBarRect = new Rect(rect.x, curY, rect.width, 24f);
|
||
|
||
string progressText;
|
||
if (totalSteps > 0)
|
||
{
|
||
progressText =
|
||
$"{(generationProgress * 100):F1}% - Step {currentStep}/{totalSteps}";
|
||
if (etaSeconds > 0)
|
||
{
|
||
progressText += $" - ETA: {etaSeconds:F0}s";
|
||
}
|
||
}
|
||
else
|
||
{
|
||
progressText = $"{(generationProgress * 100):F1}%";
|
||
}
|
||
|
||
Widgets.FillableBar(progressBarRect, (float)generationProgress);
|
||
Text.Font = GameFont.Tiny;
|
||
Text.Anchor = TextAnchor.MiddleCenter;
|
||
Widgets.Label(progressBarRect, progressText);
|
||
Text.Anchor = TextAnchor.UpperLeft;
|
||
return curY + 30f;
|
||
}
|
||
|
||
private float DrawGenerationButton(Rect rect, float curY)
|
||
{
|
||
Text.Font = GameFont.Small;
|
||
string buttonLabel = isGenerating
|
||
? "AIImages.Generation.Cancel".Translate()
|
||
: "AIImages.Generation.Generate".Translate();
|
||
|
||
// Основная кнопка генерации (занимает 40% ширины)
|
||
float buttonWidth = rect.width * 0.4f;
|
||
if (Widgets.ButtonText(new Rect(rect.x, curY, buttonWidth, 35f), buttonLabel))
|
||
{
|
||
if (isGenerating)
|
||
CancelGeneration();
|
||
else
|
||
StartGeneration();
|
||
}
|
||
|
||
// Кнопка галереи
|
||
float galleryButtonWidth = rect.width * 0.35f;
|
||
float galleryButtonX = rect.x + buttonWidth + 10f;
|
||
int imageCount = PawnPortraitHelper.GetPortraitCount(pawn);
|
||
string galleryLabel = "AIImages.Gallery.OpenGallery".Translate();
|
||
if (imageCount > 0)
|
||
{
|
||
galleryLabel += " " + "AIImages.Gallery.ImagesCount".Translate(imageCount);
|
||
}
|
||
|
||
if (Widgets.ButtonText(new Rect(galleryButtonX, curY, galleryButtonWidth, 35f), galleryLabel))
|
||
{
|
||
var galleryWindow = new Window_AIGallery(pawn);
|
||
Find.WindowStack.Add(galleryWindow);
|
||
}
|
||
|
||
// Отладочная кнопка (занимает 20% ширины)
|
||
float debugButtonWidth = rect.width * 0.2f;
|
||
float debugButtonX = galleryButtonX + galleryButtonWidth + 10f;
|
||
if (Widgets.ButtonText(new Rect(debugButtonX, curY, debugButtonWidth, 35f), "Debug"))
|
||
{
|
||
DebugAllPawns();
|
||
}
|
||
|
||
return curY + 40f;
|
||
}
|
||
}
|
||
}
|