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";
+ }
+ }
+ }
+}