feat(#144): add TokenModel POCO, TokenPanelController, and TokenRowView

This commit is contained in:
KoalaTrapLord committed 2026-03-28 19:57:00 -04:00
1 parent 9ffefd463c
commit 2c8c6c26bd
3 files changed
+229

No files matched your search

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace CommanderAILab.Models
{
/// <summary>Runtime data model for a token on the battlefield.</summary>
[Serializable]
public class TokenModel
{
public string id;
public string name;
public string power;
public string toughness;
/// <summary>Single letter: W U B R G C</summary>
public string colorCode = "C";
public int ownerSeat;
public int qty = 1;
public bool isTapped;
public Dictionary<string, int> counters = new();
public string PTString =>
(!string.IsNullOrEmpty(power) && !string.IsNullOrEmpty(toughness))
? $"{power}/{toughness}" : "";
}
}
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using CommanderAILab.Models;
namespace CommanderAILab.UI
{
/// <summary>
/// Token creation and tracking panel in the BattlefieldCenter.
/// Human creates tokens manually; AI tokens are added via AddToken() from server events.
/// </summary>
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<TokenModel> _tokens = new();
private readonly List<TokenRowView> _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<TokenRowView>();
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<TokenModel> Tokens => _tokens;
}
}
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using CommanderAILab.Models;
namespace CommanderAILab.UI
{
/// <summary>
/// A single token row in TokenPanelController's list.
/// Supports qty +/-, tap all, counter editing, and removal.
/// </summary>
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<string> CounterTypes =
new() { "+1/+1", "-1/-1", "loyalty", "charge", "quest", "time" };
private TokenModel _token;
private Action<TokenModel> _onDelete;
// ── Binding ───────────────────────────────────────────
public void Bind(TokenModel token, Action<TokenModel> 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";
}
}
}
}