diff --git a/unity-client/Assets/Scripts/Models/TokenModel.cs b/unity-client/Assets/Scripts/Models/TokenModel.cs new file mode 100644 index 0000000..c5793dc --- /dev/null +++ b/unity-client/Assets/Scripts/Models/TokenModel.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CommanderAILab.Models +{ + /// Runtime data model for a token on the battlefield. + [Serializable] + public class TokenModel + { + public string id; + public string name; + public string power; + public string toughness; + /// Single letter: W U B R G C + public string colorCode = "C"; + public int ownerSeat; + public int qty = 1; + public bool isTapped; + public Dictionary counters = new(); + + public string PTString => + (!string.IsNullOrEmpty(power) && !string.IsNullOrEmpty(toughness)) + ? $"{power}/{toughness}" : ""; + } +} diff --git a/unity-client/Assets/Scripts/UI/Battleground/TokenPanelController.cs b/unity-client/Assets/Scripts/UI/Battleground/TokenPanelController.cs new file mode 100644 index 0000000..5baf17a --- /dev/null +++ b/unity-client/Assets/Scripts/UI/Battleground/TokenPanelController.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; +using TMPro; +using CommanderAILab.Models; + +namespace CommanderAILab.UI +{ + /// + /// Token creation and tracking panel in the BattlefieldCenter. + /// Human creates tokens manually; AI tokens are added via AddToken() from server events. + /// + public class TokenPanelController : MonoBehaviour + { + [Header("Creation Row")] + [SerializeField] private TMP_InputField nameInput; + [SerializeField] private TMP_InputField powerInput; + [SerializeField] private TMP_InputField toughnessInput; + [SerializeField] private TMP_InputField qtyInput; + [SerializeField] private Button createBtn; + + [Header("List")] + [SerializeField] private Transform tokenListParent; + [SerializeField] private GameObject tokenRowPrefab; + + private readonly List _tokens = new(); + private readonly List _rowViews = new(); + + // ── Lifecycle ─────────────────────────────────────────── + private void Start() => createBtn.onClick.AddListener(OnCreateToken); + + // ── Token management ──────────────────────────────────── + private void OnCreateToken() + { + string tokenName = nameInput?.text?.Trim(); + if (string.IsNullOrEmpty(tokenName)) return; + + int.TryParse(qtyInput?.text, out int qty); + qty = Mathf.Max(1, qty); + + var token = new TokenModel + { + id = Guid.NewGuid().ToString(), + name = tokenName, + power = powerInput?.text?.Trim() ?? "", + toughness = toughnessInput?.text?.Trim() ?? "", + ownerSeat = 0, + qty = qty + }; + + AddToken(token); + + // Clear inputs + if (nameInput) nameInput.text = ""; + if (powerInput) powerInput.text = ""; + if (toughnessInput) toughnessInput.text = ""; + if (qtyInput) qtyInput.text = "1"; + } + + public void AddToken(TokenModel token) + { + _tokens.Add(token); + var go = Instantiate(tokenRowPrefab, tokenListParent); + var view = go.GetComponent(); + if (view != null) + { + view.Bind(token, RemoveToken); + _rowViews.Add(view); + } + } + + private void RemoveToken(TokenModel token) + { + int idx = _tokens.IndexOf(token); + if (idx < 0) return; + + _tokens.RemoveAt(idx); + var view = _rowViews[idx]; + _rowViews.RemoveAt(idx); + if (view != null) Destroy(view.gameObject); + } + + public IReadOnlyList Tokens => _tokens; + } +} diff --git a/unity-client/Assets/Scripts/UI/Battleground/TokenRowView.cs b/unity-client/Assets/Scripts/UI/Battleground/TokenRowView.cs new file mode 100644 index 0000000..34194c7 --- /dev/null +++ b/unity-client/Assets/Scripts/UI/Battleground/TokenRowView.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; +using TMPro; +using CommanderAILab.Models; + +namespace CommanderAILab.UI +{ + /// + /// A single token row in TokenPanelController's list. + /// Supports qty +/-, tap all, counter editing, and removal. + /// + public class TokenRowView : MonoBehaviour + { + [Header("Labels")] + [SerializeField] private TMP_Text nameLabel; + [SerializeField] private TMP_Text ptLabel; + [SerializeField] private TMP_Text qtyLabel; + [SerializeField] private TMP_Text tappedLabel; + + [Header("Buttons")] + [SerializeField] private Button qtyPlusBtn; + [SerializeField] private Button qtyMinusBtn; + [SerializeField] private Button tapAllBtn; + [SerializeField] private Button deleteBtn; + + [Header("Counter Editor")] + [SerializeField] private GameObject counterEditorPanel; + [SerializeField] private TMP_Dropdown counterTypeDropdown; + [SerializeField] private Button counterAddBtn; + [SerializeField] private Button counterRemoveBtn; + [SerializeField] private TMP_Text counterSummaryLabel; + [SerializeField] private Button toggleCounterEditorBtn; + + private static readonly List CounterTypes = + new() { "+1/+1", "-1/-1", "loyalty", "charge", "quest", "time" }; + + private TokenModel _token; + private Action _onDelete; + + // ── Binding ─────────────────────────────────────────── + public void Bind(TokenModel token, Action onDelete) + { + _token = token; + _onDelete = onDelete; + + // Wire buttons + qtyPlusBtn ?.onClick.AddListener(() => AdjustQty(+1)); + qtyMinusBtn?.onClick.AddListener(() => AdjustQty(-1)); + tapAllBtn ?.onClick.AddListener(TapAll); + deleteBtn ?.onClick.AddListener(() => _onDelete?.Invoke(_token)); + toggleCounterEditorBtn?.onClick.AddListener(ToggleCounterEditor); + counterAddBtn ?.onClick.AddListener(() => AdjustCounter(+1)); + counterRemoveBtn?.onClick.AddListener(() => AdjustCounter(-1)); + + // Populate counter type dropdown + if (counterTypeDropdown != null) + { + counterTypeDropdown.ClearOptions(); + counterTypeDropdown.AddOptions(CounterTypes); + } + + if (counterEditorPanel) counterEditorPanel.SetActive(false); + RefreshDisplay(); + } + + // ── Actions ──────────────────────────────────────────── + private void AdjustQty(int delta) + { + _token.qty = Mathf.Max(0, _token.qty + delta); + if (_token.qty == 0) { _onDelete?.Invoke(_token); return; } + RefreshDisplay(); + } + + private void TapAll() + { + _token.isTapped = !_token.isTapped; + RefreshDisplay(); + } + + private void ToggleCounterEditor() + { + if (counterEditorPanel) + counterEditorPanel.SetActive(!counterEditorPanel.activeSelf); + } + + private void AdjustCounter(int delta) + { + if (counterTypeDropdown == null) return; + string counterType = CounterTypes[Mathf.Clamp(counterTypeDropdown.value, 0, CounterTypes.Count - 1)]; + _token.counters.TryGetValue(counterType, out int current); + int next = Mathf.Max(0, current + delta); + if (next == 0) _token.counters.Remove(counterType); + else _token.counters[counterType] = next; + RefreshDisplay(); + } + + // ── Display ──────────────────────────────────────────── + private void RefreshDisplay() + { + if (nameLabel) nameLabel.text = _token.name; + if (ptLabel) ptLabel.text = _token.PTString; + if (qtyLabel) qtyLabel.text = $"×{_token.qty}"; + if (tappedLabel) tappedLabel.text = _token.isTapped ? "[Tapped]" : ""; + + // Counter summary + if (counterSummaryLabel) + { + var parts = new System.Text.StringBuilder(); + foreach (var kv in _token.counters) + parts.Append($"{kv.Key}:{kv.Value} "); + counterSummaryLabel.text = parts.Length > 0 ? parts.ToString().Trim() : "No counters"; + } + } + } +}