Files
commander-ai-lab/unity-client/Assets/Scripts/UI/DeckBuilderController.cs
T
KoalaTrapLord a501a9755d fix(unity): resolve all 32 C# compilation errors
- CardModel: add missing `id` field
- CardModel: `imageUrl` → `imageUri` (match JSON property name)
- DeckModel: `commanderName` → `commander` (match JSON property name)
- PreconDeck: `imageUrl` → `imageUri`
- Add missing `PreconListWrapper` model class
- Add `using UnityEngine.Networking` to CollectionController,
  DeckGeneratorController, PreconController
- Remove double StartCoroutine() wrapping on ApiClient calls
  (GetRaw/PatchRaw/Delete/PostRaw already return Coroutine)
  Affected: CollectionController, DeckBuilderController,
  DeckGeneratorController, ScannerController, CoachController
2026-03-19 05:13:00 +00:00

427 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using UnityEngine.Networking;
using TMPro;
using Newtonsoft.Json;
using CommanderAILab.Models;
using CommanderAILab.Services;
namespace CommanderAILab.UI
{
/// <summary>
/// DeckBuilder scene — deck list, card search, drag-to-add, mana curve,
/// color-identity pie, save/delete, export to Simulator.
/// </summary>
public class DeckBuilderController : MonoBehaviour
{
// ── Navigation ───────────────────────────────────────────────────────
[Header("Navigation")]
[SerializeField] private Button backButton;
[SerializeField] private Button exportToSimButton;
// ── Deck List Panel (left) ────────────────────────────────────────────
[Header("Deck List Panel")]
[SerializeField] private Transform deckListParent;
[SerializeField] private GameObject deckRowPrefab;
[SerializeField] private Button newDeckButton;
[SerializeField] private TMP_InputField deckNameInput;
// ── Active Deck Panel (center) ────────────────────────────────────────
[Header("Active Deck")]
[SerializeField] private TMP_Text deckTitleText;
[SerializeField] private TMP_Text cardCountText; // "99 / 100"
[SerializeField] private GameObject validationIndicator; // green/red
[SerializeField] private TMP_Text validationText;
[SerializeField] private Transform deckCardListParent;
[SerializeField] private GameObject deckCardRowPrefab;
[SerializeField] private Button saveDeckButton;
[SerializeField] private Button deleteDeckButton;
// ── Commander search ──────────────────────────────────────────────────
[Header("Commander Search")]
[SerializeField] private TMP_InputField commanderSearchInput;
[SerializeField] private Button commanderSearchButton;
[SerializeField] private TMP_Dropdown colorIdentityDropdown;
[SerializeField] private Transform commanderResultParent;
[SerializeField] private GameObject commanderResultRowPrefab;
[SerializeField] private TMP_Text selectedCommanderText;
// ── Card Search Panel (right) ─────────────────────────────────────────
[Header("Card Search Panel")]
[SerializeField] private TMP_InputField cardSearchInput;
[SerializeField] private Button cardSearchButton;
[SerializeField] private Transform cardSearchResultParent;
[SerializeField] private GameObject cardSearchRowPrefab;
// ── Mana Curve Chart ──────────────────────────────────────────────────
[Header("Mana Curve")]
[SerializeField] private Transform manaCurveBarParent; // holds bar GameObjects
[SerializeField] private GameObject manaCurveBarPrefab;
// ── Color Identity Pie ────────────────────────────────────────────────
[Header("Color Pie")]
[SerializeField] private Image[] colorPieSlices; // W U B R G C — 6 images
// ── Error ─────────────────────────────────────────────────────────────
[Header("Error")]
[SerializeField] private GameObject errorPanel;
[SerializeField] private TMP_Text errorText;
[SerializeField] private Button retryButton;
// ── State ─────────────────────────────────────────────────────────────
private List<DeckModel> _decks = new();
private DeckModel _activeDeck;
private List<CardModel> _deckCards = new();
private CardModel _commander;
private bool _isDirty;
private readonly List<GameObject> _deckRowObjects = new();
private readonly List<GameObject> _deckCardRowObjects = new();
private readonly List<GameObject> _searchRowObjects = new();
private readonly List<GameObject> _commanderRowObjects = new();
// ── Lifecycle ─────────────────────────────────────────────────────────
private void Start()
{
backButton.onClick.AddListener(() =>
{
if (_isDirty) PromptSave();
else SceneManager.LoadScene("MainMenu");
});
exportToSimButton.onClick.AddListener(OnExportToSim);
newDeckButton.onClick.AddListener(OnNewDeck);
saveDeckButton.onClick.AddListener(OnSaveDeck);
deleteDeckButton.onClick.AddListener(OnDeleteDeck);
cardSearchButton.onClick.AddListener(OnCardSearch);
cardSearchInput.onSubmit.AddListener(_ => OnCardSearch());
commanderSearchButton.onClick.AddListener(OnCommanderSearch);
commanderSearchInput.onSubmit.AddListener(_ => OnCommanderSearch());
retryButton.onClick.AddListener(LoadDecks);
errorPanel.SetActive(false);
// Pick up any card passed from Collection scene
string pending = PlayerPrefs.GetString("PendingAddCard", "");
if (!string.IsNullOrEmpty(pending))
{
PlayerPrefs.DeleteKey("PendingAddCard");
try
{
var card = JsonConvert.DeserializeObject<CardModel>(pending);
if (card != null) AddCardToDeck(card);
}
catch { }
}
LoadDecks();
}
// ── Deck List ─────────────────────────────────────────────────────────
private void LoadDecks()
{
errorPanel.SetActive(false);
ApiClient.Instance.GetDecks(
json =>
{
try
{
var wrapper = JsonConvert.DeserializeObject<DecksWrapper>(json);
_decks = wrapper?.decks ?? new List<DeckModel>();
}
catch { _decks = new List<DeckModel>(); }
RebuildDeckList();
},
err => ShowError($"Failed to load decks:\n{err}"));
}
private void RebuildDeckList()
{
foreach (var go in _deckRowObjects) if (go) Destroy(go);
_deckRowObjects.Clear();
foreach (var deck in _decks)
{
var row = Instantiate(deckRowPrefab, deckListParent);
_deckRowObjects.Add(row);
var label = row.GetComponentInChildren<TMP_Text>();
if (label) label.text = deck.name;
var btn = row.GetComponentInChildren<Button>();
var captured = deck;
if (btn) btn.onClick.AddListener(() => LoadActiveDeck(captured));
}
}
private void LoadActiveDeck(DeckModel deck)
{
_activeDeck = deck;
_deckCards = deck.cards ?? new List<CardModel>();
_isDirty = false;
RebuildDeckCardList();
RefreshCharts();
}
// ── Active Deck Cards ─────────────────────────────────────────────────
private void RebuildDeckCardList()
{
foreach (var go in _deckCardRowObjects) if (go) Destroy(go);
_deckCardRowObjects.Clear();
if (_activeDeck == null) return;
deckTitleText.text = _activeDeck.name;
UpdateCardCount();
foreach (var card in _deckCards)
{
var row = Instantiate(deckCardRowPrefab, deckCardListParent);
_deckCardRowObjects.Add(row);
var labels = row.GetComponentsInChildren<TMP_Text>();
if (labels.Length > 0) labels[0].text = card.name;
if (labels.Length > 1) labels[1].text = card.manaCost;
var btns = row.GetComponentsInChildren<Button>();
var captured = card;
// +/- buttons (assumed order: remove)
if (btns.Length > 0) btns[0].onClick.AddListener(() => RemoveCardFromDeck(captured));
}
}
private void AddCardToDeck(CardModel card)
{
if (_activeDeck == null) { ShowError("Select or create a deck first."); return; }
_deckCards.Add(card);
_isDirty = true;
RebuildDeckCardList();
RefreshCharts();
}
private void RemoveCardFromDeck(CardModel card)
{
_deckCards.Remove(card);
_isDirty = true;
RebuildDeckCardList();
RefreshCharts();
}
private void UpdateCardCount()
{
int count = _deckCards.Count;
cardCountText.text = $"{count} / 100";
bool valid = count == 100 || count == 99; // 99 + commander
validationIndicator.GetComponent<Image>().color = valid
? new Color(0.2f, 0.7f, 0.2f) : new Color(0.7f, 0.2f, 0.2f);
validationText.text = valid ? "Valid deck" : $"{100 - count} cards needed";
}
// ── Card Search (right panel) ─────────────────────────────────────────
private void OnCardSearch()
{
string q = cardSearchInput.text.Trim();
if (string.IsNullOrEmpty(q)) return;
ApiClient.Instance.GetCollection(
json => PopulateCardSearchResults(json),
err => ShowError($"Search failed:\n{err}"),
search: q);
}
private void PopulateCardSearchResults(string json)
{
foreach (var go in _searchRowObjects) if (go) Destroy(go);
_searchRowObjects.Clear();
List<CardModel> cards;
try
{
var resp = JsonConvert.DeserializeObject<CollectionResponse>(json);
cards = resp?.cards ?? new List<CardModel>();
}
catch { return; }
foreach (var card in cards)
{
var row = Instantiate(cardSearchRowPrefab, cardSearchResultParent);
_searchRowObjects.Add(row);
var label = row.GetComponentInChildren<TMP_Text>();
if (label) label.text = $"{card.name} {card.manaCost}";
var btn = row.GetComponentInChildren<Button>();
var captured = card;
if (btn) btn.onClick.AddListener(() => AddCardToDeck(captured));
}
}
// ── Commander Search ──────────────────────────────────────────────────
private void OnCommanderSearch()
{
string q = commanderSearchInput.text.Trim();
string color = colorIdentityDropdown.value > 0
? colorIdentityDropdown.options[colorIdentityDropdown.value].text : "";
string path = $"/api/collection?search={UnityWebRequest.EscapeURL(q)}&type=Legendary+Creature";
if (!string.IsNullOrEmpty(color))
path += $"&colors={UnityWebRequest.EscapeURL(color)}";
ApiClient.Instance.GetRaw(path,
json => PopulateCommanderResults(json),
err => ShowError($"Commander search failed:\n{err}"));
}
private void PopulateCommanderResults(string json)
{
foreach (var go in _commanderRowObjects) if (go) Destroy(go);
_commanderRowObjects.Clear();
List<CardModel> cards;
try
{
var resp = JsonConvert.DeserializeObject<CollectionResponse>(json);
cards = resp?.cards ?? new List<CardModel>();
}
catch { return; }
foreach (var card in cards)
{
var row = Instantiate(commanderResultRowPrefab, commanderResultParent);
_commanderRowObjects.Add(row);
var label = row.GetComponentInChildren<TMP_Text>();
if (label) label.text = card.name;
var btn = row.GetComponentInChildren<Button>();
var captured = card;
if (btn) btn.onClick.AddListener(() => SelectCommander(captured));
}
}
private void SelectCommander(CardModel card)
{
_commander = card;
selectedCommanderText.text = $"Commander: {card.name}";
if (_activeDeck != null) _activeDeck.commander = card.name;
_isDirty = true;
}
// ── Save / Delete ─────────────────────────────────────────────────────
private void OnNewDeck()
{
string name = deckNameInput.text.Trim();
if (string.IsNullOrEmpty(name)) name = "New Deck";
var newDeck = new DeckModel { name = name, cards = new List<CardModel>() };
string json = JsonConvert.SerializeObject(newDeck);
ApiClient.Instance.CreateDeck(json,
_ => LoadDecks(),
err => ShowError($"Create deck failed:\n{err}"));
}
private void OnSaveDeck()
{
if (_activeDeck == null) return;
_activeDeck.cards = _deckCards;
string json = JsonConvert.SerializeObject(_activeDeck);
string path = $"/api/decks/{_activeDeck.id}";
ApiClient.Instance.PatchRaw(path, json,
_ => { _isDirty = false; LoadDecks(); },
err => ShowError($"Save failed:\n{err}"));
}
private void OnDeleteDeck()
{
if (_activeDeck == null) return;
ApiClient.Instance.Delete($"/api/decks/{_activeDeck.id}",
_ => { _activeDeck = null; _deckCards.Clear(); LoadDecks(); },
err => ShowError($"Delete failed:\n{err}"));
}
// ── Charts ────────────────────────────────────────────────────────────
private void RefreshCharts()
{
BuildManaCurve();
BuildColorPie();
}
private void BuildManaCurve()
{
foreach (Transform child in manaCurveBarParent) Destroy(child.gameObject);
int[] buckets = new int[8]; // 0,1,2,3,4,5,6,7+
foreach (var card in _deckCards)
{
int cmc = Mathf.Clamp(Mathf.RoundToInt(card.cmc), 0, 7);
buckets[cmc]++;
}
int max = 1;
foreach (int v in buckets) if (v > max) max = v;
for (int i = 0; i < buckets.Length; i++)
{
var bar = Instantiate(manaCurveBarPrefab, manaCurveBarParent);
var rt = bar.GetComponent<RectTransform>();
float height = (buckets[i] / (float)max) * 120f;
rt.sizeDelta = new Vector2(rt.sizeDelta.x, height);
var label = bar.GetComponentInChildren<TMP_Text>();
if (label) label.text = buckets[i].ToString();
}
}
private void BuildColorPie()
{
if (colorPieSlices == null || colorPieSlices.Length == 0) return;
// Count WUBRGC from mana costs
int[] counts = new int[6]; // W U B R G C
foreach (var card in _deckCards)
{
string mc = card.manaCost ?? "";
if (mc.Contains("W")) counts[0]++;
if (mc.Contains("U")) counts[1]++;
if (mc.Contains("B")) counts[2]++;
if (mc.Contains("R")) counts[3]++;
if (mc.Contains("G")) counts[4]++;
// colorless: has digits but no WUBRG
if (!mc.Contains("W") && !mc.Contains("U") && !mc.Contains("B")
&& !mc.Contains("R") && !mc.Contains("G") && mc.Length > 0)
counts[5]++;
}
int total = 0;
foreach (int c in counts) total += c;
if (total == 0) total = 1;
for (int i = 0; i < colorPieSlices.Length && i < counts.Length; i++)
{
colorPieSlices[i].fillAmount = counts[i] / (float)total;
}
}
// ── Error / Prompt ────────────────────────────────────────────────────
private void ShowError(string msg)
{
errorPanel.SetActive(true);
errorText.text = msg;
}
private void PromptSave()
{
// Simple: just save and navigate
OnSaveDeck();
SceneManager.LoadScene("MainMenu");
}
private void OnExportToSim()
{
if (_activeDeck == null) return;
PlayerPrefs.SetString("SimDeck", JsonConvert.SerializeObject(_activeDeck));
SceneManager.LoadScene("Simulator");
}
// ── Helper Models ─────────────────────────────────────────────────────
[System.Serializable]
private class DecksWrapper { public List<DeckModel> decks; }
[System.Serializable]
private class CollectionResponse { public List<CardModel> cards; }
}
}