feat(#144): add TokenModel POCO, TokenPanelController, and TokenRowView
This commit is contained in:
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user