Files
commander-ai-lab/unity-client/Assets/Editor/CreateSimulatorScene.cs
T

446 lines
23 KiB
C#

#if UNITY_EDITOR
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
using UnityEditor.SceneManagement;
using TMPro;
using System.Reflection;
using System.Linq;
public static class CreateSimulatorScene
{
[MenuItem("Tools/Create Simulator Scene")]
public static void Create()
{
var scene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);
// ── Services ──────────────────────────────────────────────────────────
var services = new GameObject("[Services]");
services.AddComponent<CommanderAILab.Services.ApiClient>();
// ── Canvas ────────────────────────────────────────────────────────────
var canvasGo = new GameObject("Canvas");
var canvas = canvasGo.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
var scaler = canvasGo.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
canvasGo.AddComponent<GraphicRaycaster>();
if (Object.FindObjectOfType<UnityEngine.EventSystems.EventSystem>() == null)
{
var es = new GameObject("EventSystem");
es.AddComponent<UnityEngine.EventSystems.EventSystem>();
es.AddComponent<UnityEngine.EventSystems.StandaloneInputModule>();
}
// ── Background ────────────────────────────────────────────────────────
CreatePanel(canvasGo.transform, "Background", new Color(0.08f, 0.08f, 0.12f), stretch: true);
// ── Header (Top Bar) ──────────────────────────────────────────────────
var header = CreatePanel(canvasGo.transform, "Header", new Color(0.12f, 0.12f, 0.18f, 0.95f));
SetAnchors(header.GetComponent<RectTransform>(), new Vector2(0f, 0.93f), new Vector2(1f, 1f));
var backBtn = CreateTMPButton(header.transform, "BackButton", "< Back", new Color(0.2f, 0.2f, 0.3f));
SetAnchors(backBtn.GetComponent<RectTransform>(), new Vector2(0.01f, 0.1f), new Vector2(0.08f, 0.9f));
var titleTxt = CreateTMPText(header.transform, "TitleText", "Simulator", 28, FontStyles.Bold, TextAlignmentOptions.Center);
SetAnchors(titleTxt.GetComponent<RectTransform>(), new Vector2(0.3f, 0.1f), new Vector2(0.7f, 0.9f));
var startSimBtn = CreateTMPButton(header.transform, "StartSimButton", "Start Simulation", new Color(0.2f, 0.55f, 0.2f));
SetAnchors(startSimBtn.GetComponent<RectTransform>(), new Vector2(0.82f, 0.1f), new Vector2(0.99f, 0.9f));
// ── Config Panel (left) ───────────────────────────────────────────────
var configPanel = CreatePanel(canvasGo.transform, "ConfigPanel", new Color(0.1f, 0.1f, 0.16f, 0.97f));
SetAnchors(configPanel.GetComponent<RectTransform>(), new Vector2(0.01f, 0.3f), new Vector2(0.49f, 0.93f));
var cfgVlg = configPanel.AddComponent<VerticalLayoutGroup>();
cfgVlg.padding = new RectOffset(16, 16, 16, 16);
cfgVlg.spacing = 10;
cfgVlg.childForceExpandWidth = true;
cfgVlg.childForceExpandHeight = false;
CreateTMPText(configPanel.transform, "ConfigTitle", "Simulation Config", 20, FontStyles.Bold, TextAlignmentOptions.Center);
// Deck dropdowns
var deckDropdown0 = CreateTMPDropdown(configPanel.transform, "DeckDropdown0", new[] { "-- Select Deck --" });
var deckDropdown1 = CreateTMPDropdown(configPanel.transform, "DeckDropdown1", new[] { "-- Select Deck --" });
var deckDropdown2 = CreateTMPDropdown(configPanel.transform, "DeckDropdown2", new[] { "-- Select Deck --" });
var deckDropdown3 = CreateTMPDropdown(configPanel.transform, "DeckDropdown3", new[] { "-- Select Deck --" });
// Num players slider
var numPlayersLabel = CreateTMPText(configPanel.transform, "NumPlayersLabel", "Players: 2", 16, FontStyles.Normal, TextAlignmentOptions.Left);
var numPlayersSlider = CreateSlider(configPanel.transform, "NumPlayersSlider");
// Num games input
CreateTMPText(configPanel.transform, "NumGamesLabel", "Number of Games:", 16, FontStyles.Normal, TextAlignmentOptions.Left);
var numGamesInput = CreateTMPInputField(configPanel.transform, "NumGamesInput", "100");
// Threads input
CreateTMPText(configPanel.transform, "ThreadsLabel", "Threads:", 16, FontStyles.Normal, TextAlignmentOptions.Left);
var threadsInput = CreateTMPInputField(configPanel.transform, "ThreadsInput", "4");
// AI Profile dropdown
CreateTMPText(configPanel.transform, "AiProfileLabel", "AI Profile:", 16, FontStyles.Normal, TextAlignmentOptions.Left);
var aiProfileDropdown = CreateTMPDropdown(configPanel.transform, "AiProfileDropdown", new[] { "Default" });
// ── Status Panel (center, hidden initially) ───────────────────────────
var statusPanel = CreatePanel(canvasGo.transform, "StatusPanel", new Color(0.09f, 0.09f, 0.14f, 0.97f));
SetAnchors(statusPanel.GetComponent<RectTransform>(), new Vector2(0.01f, 0.06f), new Vector2(0.49f, 0.29f));
statusPanel.SetActive(false);
var stVlg = statusPanel.AddComponent<VerticalLayoutGroup>();
stVlg.padding = new RectOffset(12, 12, 10, 10);
stVlg.spacing = 6;
stVlg.childForceExpandWidth = true;
stVlg.childForceExpandHeight = false;
var statusText = CreateTMPText(statusPanel.transform, "StatusText", "Ready", 16, FontStyles.Normal, TextAlignmentOptions.TopLeft);
var progressBar = CreateSlider(statusPanel.transform, "ProgressBar");
var progressText = CreateTMPText(statusPanel.transform, "ProgressText", "0%", 14, FontStyles.Normal, TextAlignmentOptions.Center);
var elapsedText = CreateTMPText(statusPanel.transform, "ElapsedText", "Elapsed: 0s", 14, FontStyles.Normal, TextAlignmentOptions.Left);
var simsPerSecText = CreateTMPText(statusPanel.transform, "SimsPerSecText", "0 sims/s", 14, FontStyles.Normal, TextAlignmentOptions.Left);
// ── Results Panel (right, hidden initially) ───────────────────────────
var resultsPanel = CreatePanel(canvasGo.transform, "ResultsPanel", new Color(0.1f, 0.1f, 0.16f, 0.97f));
SetAnchors(resultsPanel.GetComponent<RectTransform>(), new Vector2(0.51f, 0.06f), new Vector2(0.99f, 0.93f));
resultsPanel.SetActive(false);
CreateTMPText(resultsPanel.transform, "ResultsTitle", "Results", 22, FontStyles.Bold, TextAlignmentOptions.Center);
var winnerText = CreateTMPText(resultsPanel.transform, "WinnerText", "Winner: —", 18, FontStyles.Bold, TextAlignmentOptions.Center);
SetAnchors(winnerText.GetComponent<RectTransform>(), new Vector2(0f, 0.88f), new Vector2(1f, 0.95f));
// Results scroll view
var resultsScroll = new GameObject("ResultsScroll");
resultsScroll.transform.SetParent(resultsPanel.transform, false);
SetAnchors(resultsScroll.GetComponent<RectTransform>(), new Vector2(0f, 0.15f), new Vector2(1f, 0.88f));
ScrollRect resSr;
var resultsContent = CreateScrollContent(resultsScroll, out resSr);
var historyBtn = CreateTMPButton(resultsPanel.transform, "HistoryButton", "View History", new Color(0.2f, 0.4f, 0.7f));
SetAnchors(historyBtn.GetComponent<RectTransform>(), new Vector2(0.2f, 0.03f), new Vector2(0.8f, 0.13f));
// ── Error Panel (hidden initially) ────────────────────────────────────
var errorPanel = CreatePanel(canvasGo.transform, "ErrorPanel", new Color(0.15f, 0.05f, 0.05f, 0.97f));
SetAnchors(errorPanel.GetComponent<RectTransform>(), new Vector2(0.25f, 0.35f), new Vector2(0.75f, 0.65f));
errorPanel.SetActive(false);
CreateTMPText(errorPanel.transform, "ErrorTitle", "Error", 22, FontStyles.Bold, TextAlignmentOptions.Center);
var errorText = CreateTMPText(errorPanel.transform, "ErrorText", "", 16, FontStyles.Normal, TextAlignmentOptions.TopLeft);
SetAnchors(errorText.GetComponent<RectTransform>(), new Vector2(0.05f, 0.25f), new Vector2(0.95f, 0.85f));
var retryBtn = CreateTMPButton(errorPanel.transform, "RetryButton", "Retry", new Color(0.55f, 0.2f, 0.2f));
SetAnchors(retryBtn.GetComponent<RectTransform>(), new Vector2(0.3f, 0.05f), new Vector2(0.7f, 0.22f));
// ── Result Row Prefab (disabled child object used as prefab) ──────────
var resultRowPrefab = new GameObject("ResultRowPrefab");
resultRowPrefab.transform.SetParent(canvasGo.transform, false);
resultRowPrefab.SetActive(false);
var rowImg = resultRowPrefab.AddComponent<Image>();
rowImg.color = new Color(0.15f, 0.15f, 0.2f, 0.9f);
var rowHlg = resultRowPrefab.AddComponent<HorizontalLayoutGroup>();
rowHlg.padding = new RectOffset(8, 8, 4, 4);
rowHlg.spacing = 12;
rowHlg.childForceExpandWidth = false;
rowHlg.childForceExpandHeight = true;
var rowLe = resultRowPrefab.AddComponent<LayoutElement>();
rowLe.minHeight = 40;
string[] colNames = { "DeckName", "WinRate", "Wins", "Losses" };
float[] colWidths = { 300f, 120f, 100f, 100f };
for (int i = 0; i < colNames.Length; i++)
{
var cell = new GameObject(colNames[i]);
cell.transform.SetParent(resultRowPrefab.transform, false);
var cellTxt = cell.AddComponent<TextMeshProUGUI>();
cellTxt.text = colNames[i];
cellTxt.fontSize = 16;
cellTxt.color = Color.white;
cellTxt.alignment = TextAlignmentOptions.Left;
var cellLe = cell.AddComponent<LayoutElement>();
cellLe.minWidth = colWidths[i];
cellLe.preferredWidth = colWidths[i];
cellLe.flexibleWidth = i == 0 ? 1f : 0f;
}
// ── Wire SimulationController via reflection ───────────────────────────
var controllerGo = new GameObject("SimulationController");
controllerGo.transform.SetParent(canvasGo.transform, false);
var controller = controllerGo.AddComponent<CommanderAILab.UI.SimulationController>();
SetField(controller, "backButton", backBtn.GetComponent<Button>());
SetField(controller, "startSimButton", startSimBtn.GetComponent<Button>());
SetField(controller, "deckDropdown0", deckDropdown0.GetComponent<TMP_Dropdown>());
SetField(controller, "deckDropdown1", deckDropdown1.GetComponent<TMP_Dropdown>());
SetField(controller, "deckDropdown2", deckDropdown2.GetComponent<TMP_Dropdown>());
SetField(controller, "deckDropdown3", deckDropdown3.GetComponent<TMP_Dropdown>());
SetField(controller, "numGamesInput", numGamesInput.GetComponent<TMP_InputField>());
SetField(controller, "numPlayersSlider", numPlayersSlider.GetComponent<Slider>());
SetField(controller, "numPlayersLabel", numPlayersLabel.GetComponent<TMP_Text>());
SetField(controller, "threadsInput", threadsInput.GetComponent<TMP_InputField>());
SetField(controller, "aiProfileDropdown", aiProfileDropdown.GetComponent<TMP_Dropdown>());
SetField(controller, "statusText", statusText.GetComponent<TMP_Text>());
SetField(controller, "progressBar", progressBar.GetComponent<Slider>());
SetField(controller, "progressText", progressText.GetComponent<TMP_Text>());
SetField(controller, "elapsedText", elapsedText.GetComponent<TMP_Text>());
SetField(controller, "simsPerSecText", simsPerSecText.GetComponent<TMP_Text>());
SetField(controller, "resultsParent", resultsContent);
SetField(controller, "resultRowPrefab", resultRowPrefab);
SetField(controller, "winnerText", winnerText.GetComponent<TMP_Text>());
SetField(controller, "historyButton", historyBtn.GetComponent<Button>());
SetField(controller, "errorPanel", errorPanel);
SetField(controller, "errorText", errorText.GetComponent<TMP_Text>());
SetField(controller, "retryButton", retryBtn.GetComponent<Button>());
// ── Save ──────────────────────────────────────────────────────────────
if (!AssetDatabase.IsValidFolder("Assets/Scenes"))
AssetDatabase.CreateFolder("Assets", "Scenes");
string path = "Assets/Scenes/Simulator.unity";
EditorSceneManager.SaveScene(scene, path);
var existing = new System.Collections.Generic.List<EditorBuildSettingsScene>(EditorBuildSettings.scenes);
if (!existing.Any(s => s.path == path))
existing.Add(new EditorBuildSettingsScene(path, true));
EditorBuildSettings.scenes = existing.ToArray();
Debug.Log("[CreateSimulatorScene] Simulator scene created and saved to " + path);
}
// ── Helpers ───────────────────────────────────────────────────────────────
static void SetField(object target, string fieldName, object value)
{
var field = target.GetType().GetField(fieldName,
BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null) field.SetValue(target, value);
else Debug.LogWarning("[CreateSimulatorScene] Could not find field: " + fieldName);
}
static void SetAnchors(RectTransform rt, Vector2 min, Vector2 max)
{
rt.anchorMin = min;
rt.anchorMax = max;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
}
static GameObject CreatePanel(Transform parent, string name, Color color, bool stretch = false)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
var img = go.AddComponent<Image>();
img.color = color;
var rt = go.GetComponent<RectTransform>();
if (stretch) SetAnchors(rt, Vector2.zero, Vector2.one);
return go;
}
static GameObject CreateTMPText(Transform parent, string name, string text, int fontSize, FontStyles style, TextAlignmentOptions alignment)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
var tmp = go.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = fontSize;
tmp.fontStyle = style;
tmp.alignment = alignment;
tmp.color = Color.white;
var rt = go.GetComponent<RectTransform>();
SetAnchors(rt, Vector2.zero, Vector2.one);
return go;
}
static GameObject CreateTMPButton(Transform parent, string name, string label, Color bgColor)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
var img = go.AddComponent<Image>();
img.color = bgColor;
var btn = go.AddComponent<Button>();
btn.targetGraphic = img;
var colors = btn.colors;
colors.highlightedColor = bgColor * 1.2f;
colors.pressedColor = bgColor * 0.8f;
btn.colors = colors;
var textGo = new GameObject("Text");
textGo.transform.SetParent(go.transform, false);
var tmp = textGo.AddComponent<TextMeshProUGUI>();
tmp.text = label;
tmp.fontSize = 22;
tmp.alignment = TextAlignmentOptions.Center;
tmp.color = Color.white;
SetAnchors(textGo.GetComponent<RectTransform>(), Vector2.zero, Vector2.one);
var le = go.AddComponent<LayoutElement>();
le.minHeight = 50;
return go;
}
static GameObject CreateTMPInputField(Transform parent, string name, string defaultText)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
var img = go.AddComponent<Image>();
img.color = new Color(0.15f, 0.15f, 0.2f);
var textArea = new GameObject("Text Area");
textArea.transform.SetParent(go.transform, false);
textArea.AddComponent<RectMask2D>();
SetAnchors(textArea.GetComponent<RectTransform>(), Vector2.zero, Vector2.one);
var placeholder = new GameObject("Placeholder");
placeholder.transform.SetParent(textArea.transform, false);
var phTmp = placeholder.AddComponent<TextMeshProUGUI>();
phTmp.text = defaultText;
phTmp.fontSize = 18;
phTmp.fontStyle = FontStyles.Italic;
phTmp.color = new Color(0.5f, 0.5f, 0.5f, 0.8f);
phTmp.alignment = TextAlignmentOptions.Left;
SetAnchors(placeholder.GetComponent<RectTransform>(), Vector2.zero, Vector2.one);
var inputText = new GameObject("Text");
inputText.transform.SetParent(textArea.transform, false);
var itTmp = inputText.AddComponent<TextMeshProUGUI>();
itTmp.fontSize = 18;
itTmp.color = Color.white;
itTmp.alignment = TextAlignmentOptions.Left;
SetAnchors(inputText.GetComponent<RectTransform>(), Vector2.zero, Vector2.one);
var inputField = go.AddComponent<TMP_InputField>();
inputField.textViewport = textArea.GetComponent<RectTransform>();
inputField.textComponent = itTmp;
inputField.placeholder = phTmp;
inputField.text = defaultText;
inputField.fontAsset = itTmp.font;
var le = go.AddComponent<LayoutElement>();
le.minHeight = 45;
return go;
}
static GameObject CreateTMPDropdown(Transform parent, string name, string[] options)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
var img = go.AddComponent<Image>();
img.color = new Color(0.15f, 0.15f, 0.2f);
var labelGo = new GameObject("Label");
labelGo.transform.SetParent(go.transform, false);
var labelTmp = labelGo.AddComponent<TextMeshProUGUI>();
labelTmp.text = options.Length > 0 ? options[0] : "";
labelTmp.fontSize = 16;
labelTmp.alignment = TextAlignmentOptions.Left;
labelTmp.color = Color.white;
SetAnchors(labelGo.GetComponent<RectTransform>(), Vector2.zero, Vector2.one);
var dd = go.AddComponent<TMP_Dropdown>();
dd.captionText = labelTmp;
dd.ClearOptions();
dd.AddOptions(new System.Collections.Generic.List<string>(options));
var le = go.AddComponent<LayoutElement>();
le.minHeight = 40;
return go;
}
static GameObject CreateSlider(Transform parent, string name)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
// Background track
var bg = new GameObject("Background");
bg.transform.SetParent(go.transform, false);
var bgImg = bg.AddComponent<Image>();
bgImg.color = new Color(0.2f, 0.2f, 0.25f);
SetAnchors(bg.GetComponent<RectTransform>(), new Vector2(0f, 0.25f), new Vector2(1f, 0.75f));
// Fill area
var fillArea = new GameObject("Fill Area");
fillArea.transform.SetParent(go.transform, false);
SetAnchors(fillArea.GetComponent<RectTransform>(), new Vector2(0f, 0.25f), new Vector2(1f, 0.75f));
var fill = new GameObject("Fill");
fill.transform.SetParent(fillArea.transform, false);
var fillImg = fill.AddComponent<Image>();
fillImg.color = new Color(0.2f, 0.6f, 0.3f);
var fillRt = fill.GetComponent<RectTransform>();
fillRt.anchorMin = Vector2.zero;
fillRt.anchorMax = Vector2.one;
fillRt.offsetMin = Vector2.zero;
fillRt.offsetMax = Vector2.zero;
// Handle
var handleArea = new GameObject("Handle Slide Area");
handleArea.transform.SetParent(go.transform, false);
SetAnchors(handleArea.GetComponent<RectTransform>(), Vector2.zero, Vector2.one);
var handle = new GameObject("Handle");
handle.transform.SetParent(handleArea.transform, false);
var handleImg = handle.AddComponent<Image>();
handleImg.color = new Color(0.8f, 0.8f, 0.9f);
var handleRt = handle.GetComponent<RectTransform>();
handleRt.sizeDelta = new Vector2(20f, 20f);
var slider = go.AddComponent<Slider>();
slider.fillRect = fillRt;
slider.handleRect = handleRt;
slider.targetGraphic = handleImg;
slider.direction = Slider.Direction.LeftToRight;
slider.minValue = 0f;
slider.maxValue = 1f;
slider.value = 0f;
var le = go.AddComponent<LayoutElement>();
le.minHeight = 30;
return go;
}
/// <summary>
/// Creates a Viewport + Content hierarchy inside the given scroll GameObject,
/// wires up the ScrollRect, and returns the Content RectTransform.
/// </summary>
static RectTransform CreateScrollContent(GameObject scrollGo, out ScrollRect scrollRect)
{
scrollRect = scrollGo.GetComponent<ScrollRect>();
if (scrollRect == null)
scrollRect = scrollGo.AddComponent<ScrollRect>();
var viewport = new GameObject("Viewport");
viewport.transform.SetParent(scrollGo.transform, false);
viewport.AddComponent<RectMask2D>();
var vpRect = viewport.GetComponent<RectTransform>();
vpRect.anchorMin = Vector2.zero;
vpRect.anchorMax = Vector2.one;
vpRect.offsetMin = Vector2.zero;
vpRect.offsetMax = Vector2.zero;
var content = new GameObject("Content");
content.transform.SetParent(viewport.transform, false);
var contentRect = content.AddComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0f, 1f);
contentRect.anchorMax = new Vector2(1f, 1f);
contentRect.pivot = new Vector2(0.5f, 1f);
contentRect.offsetMin = Vector2.zero;
contentRect.offsetMax = Vector2.zero;
content.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var vlg = content.AddComponent<VerticalLayoutGroup>();
vlg.spacing = 4;
vlg.padding = new RectOffset(4, 4, 4, 4);
vlg.childForceExpandWidth = true;
vlg.childForceExpandHeight = false;
scrollRect.viewport = vpRect;
scrollRect.content = contentRect;
scrollRect.horizontal = false;
return contentRect;
}
}
#endif